参考链接:哔哩哔哩视频,在线文章。
首先下载安装包,Golang镜像网站,Golang中文镜像网站。Windows直接安装,Linux解压到要安装的文件夹下即可。
设置环境变量
GOROOT,设置为安装路径即可,例如:D:\Program Files\go
GOPATH,即我们写go语言的工作路径,可以自定义,例如:D:\Program Files\go\GoWorks
然后在path中加上%GOROOT\bin
验证是否安装和配置成功:
go version
IDE如果是免费的选择VSCode,收费的选择Goland。也可以Vim+go插件。
1、极简单的部署方式
可直接编译成机器码,不依赖其他库,可以直接运行部署。
2、静态类型语言
编译的时候可以检查出隐藏的大多数问题(一般静态语言的优势)。
3、语言层面的并发
go语言是基因支持的并发,很多语言其实是“美容”的并发,一层包一层实现高并发。go的语法使得能够充分地利用多核,切换成本低,尽量提高CPU并发效率。
4、强大的标准库支撑
有runtime的系统调度机制,高效的垃圾回收,丰富的标准库。
5、简单易学
仅有25个关键字,语法从c语言过渡,内嵌c语法支持,具有面向对象特征(继承、封装、多态),跨平台。
6、大厂领军
国内外大公司有在使用go语言,例如Google,Facebook,腾讯,百度,字节跳动,京东,小米,阿里巴巴,哔哩哔哩等等。
例子:斐波那契亚数列算法下不同语言效率对比
就这个例子而言,可以看到不管是编译还是运行Go都是比较快的。
1、云计算基础设施领域
2、基础后端软件
3、微服务
4、互联网基础设施
1、包管理,大部分第三方库都在Github上,代码稳定性有风险
2、无泛化类型,目前在Go1.18已经加上了泛型
3、全部Exception都采用Error处理
Java是极端地把全部Error都用Exception处理,python是取了中间两种都可以,而Golang是极端地把全部Exception都用Error处理,没有谁对谁错之分
4、对C语言的降级处理并不是无缝的,没有降级到汇编那么完美,但目前只有go能够这样做
c语言是唯一能够和操作系统交流的语言,
package main
import "fmt"
func main() {
fmt.Println("Hello Go!")
}
然后使用go run hello.go
既编译又运行,或者先go build hello.go
会生成可执行程序,再.\hello
执行。
1、有没有分号都可以,对编译影响不大,建议不加
2、导包方式有两种,导入多个包建议后者
import "fmt"
import "time"
或者
import (
"fmt"
"time"
)
3、方法的左花括号必须要和函数名同一行
1、声明一个变量,不初始化,默认值是0
var a int
2、声明一个变量,并初始化
var b int = 100
3、初始化时省去类型声明,通过值的类型自动匹配(不推荐)
var c = 100
fmt.Printf("%T", c)
4、(常用)省去var关键字,直接匹配
d := 100
区别:声明全局变量(方法外)可以用前三种,不能用第四种
1、数据类型相同
var x, y int = 100, 200
2、数据类型不同
var k, l = 100, "hello"
3、多行写法,类型可不声明
var (
i (int) = 100
j (bool) = true
)
常量命名
const length int = 10
const定义枚举
const (
BEIJING = 0
SHANGHAI = 1
SHENZHEN = 2
)
或者使用iota,只要第一个赋值iota,它默认是0,每行依次加1
const (
BEIJING = iota
SHANGHAI
SHENZHEN
)
如果写成10*iota
,则依次是0,10,20。相当于后面都是符合前面的iota表达式,如果中间改变表达式,后面也会改变,但是iota累加的值保持。
const (
a, b = 1 + iota, 2 + iota
c, d
e, f
g, h = 2 * iota, 3 * iota
i, j
)
fmt.Println(a, b, c, d, e, f, g, h, i, j)
函数声明,括号内是形参,右边是返回值
func f1(a string, b int) int {
fmt.Println(a)
fmt.Println(b)
c := 100
return c
}
多返回值,用括号括起来
1、匿名返回值
func f2(a string, b int) (int, int) {
fmt.Println(a)
fmt.Println(b)
return 111, 222
}
2、给返回值命名
func f3(a string, b int) (r1 int, r2 int) {
fmt.Println(a)
fmt.Println(b)
r1 = 111
r2 = 222
return
}
3、如果返回值类型一样,可以只保留一个返回值类型
func f4(a string, b int) (r1, r2 int) {
fmt.Println(a)
fmt.Println(b)
r1 = 111
r2 = 222
return
}
1、如果不想使用某一个包的API,但是要使用这个包的init函数,可以匿名导包
import _ "lib1"
2、可以给包起别名,并且可以调用它的方法
import mylib2 "lib2"
3、将包的全部方法导入本包,调用方法时可以不带包名(少使用,防止函数名冲突)
import . lib2
默认情况下方法是值传递
package main
import "fmt"
func main() {
a := 1
changeValue(a)
fmt.Println(a)//输出为20,说明a的值未改变
}
func changeValue(p int) {
p = 10
}
可以指针传递,*int表示是指向int类型的指针,p处存储的就是a的地址值,*p表示找到存的地址值对应的地址,然后改变值为10,&表示传入地址
package main
import "fmt"
func main() {
a := 1
changeValue(&a)
fmt.Println(a)//输出10,说明a的值被改变
}
func changeValue(p *int) {
*p = 10
}
经典例子:交换数据
如果这样交换,并不能交换成功,因为是值传递
package main
import "fmt"
func main() {
a := 10
b := 20
swap(a, b)
fmt.Println(a, b)
}
func swap(a int, b int) {
tmp := a
a = b
b = tmp
}
需要使用指针
package main
import "fmt"
func main() {
a := 10
b := 20
swap(&a, &b)
fmt.Println(a, b)
}
func swap(a *int, b *int) {
tmp := *a
*a = *b
*b = tmp
}
二级指针:指针的指针
有点像c++的析构函数或者Java中的finally关键字
defer语句放在return之前,在当前函数结束,return返回后执行,defer可以有多个,但是按照栈的顺序,先写的后执行。
package main
import "fmt"
func main() {
returnAndDefer()
}
func returnFun() int {
fmt.Println("return...")
return 0
}
func returnAndDefer() int {
defer fmt.Println("defer...")
return returnFun()
}
声明数组
var myArray1 [10]int
myArray2 := [10]int{}
数组如果作为形参,要声明长度,而且是值拷贝,方法内不改变数组的值
func printArray(myArray [10]int) {
for i, value := range myArray {
fmt.Println(i,value)
}
}
动态数组声明,相比之下不指定长度
var myArray1 []int
myArray1 := []int{1,2,3,4}
方法如下,动态数组是指针传递,因此方法内会改变原有的值
func printArray(myArray []int) {
for i, value := range myArray {
fmt.Println(i, value)
}
}
另外,再使用range遍历时,如果索引不想使用,可以使用匿名的方式for _, value := range myArray
四种声明slice方式如下:
slice1 := []int{1, 2, 3}
var slice2 []int
var slice3 []int = make([]int,3)//通过make分配空间
slice4 := make([]int,3)
if slice2 == nil {
fmt.Println("空切片")
} else {
fmt.Println("有空间")
}
输出空切片,要注意else要和上一个右花括号放在同一行。
1、切片容量的追加
长度小于等于容量
append方法追加一个元素并赋值,如果cap不够会追加cap容量
var slice = make([]int, 3, 5)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 1)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 2)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 3)
fmt.Println(len(slice), cap(slice), slice)
扩容机制:根据cap增加二倍,即每次翻一倍
2、切片的截取
这里和python有些类似,但是这里是指针传递,改变slice2会改变slice。
slice1 := slice[0:2]//取头不取尾
slice2 := slice[:5]
slice3 := slice[3:]
slice4 := slice[:]
copy函数可以深拷贝,前一个参数是destination,后一个参数是source
slice := []int{0, 1, 2, 3, 4, 5, 6}
slice5 := make([]int,7)
copy(slice5,slice)
三种声明方式:
var myMap1 map[string]int
myMap1 = make(map[string]string, 10)
myMap1["one"] = "java"
myMap1["two"] = "c"
myMap1["three"] = "python"
myMap2 := make(map[string]string)
myMap3 := map[string]string{
"one": "c++",
"two": "java",
"three": "python",
}
使用方式:
//遍历
for key, value := range cityMap {
fmt.Println(key)
fmt.Println(value)
}
//删除
delete(cityMap, "Japan")
//修改
cityMap["USA"] = "Washington"
type声明一种新的数据类型,可以定义结构体,即把多种基本数据类型组合形成复杂的数据类型。%v可以格式化各种类型的输出。
type Book struct {
title string
autu string
}
var book1 Book
book1.title = "Golang"
book1.autu = "zhangsan"
fmt.Printf("%v\n", book1)
如果需要函数中改变值,需要传指针
changeBook(&book1)
func changeBook(book *Book) {
book.auth = "666"
}
go语言中的类其实就是结构体绑定方法,
type Hero struct {
Name string
Ad int
Level string
}
func (this Hero) GetName() {
fmt.Println("Name = " + this.Name)
}
func (this Hero) SetName(newName string) {
this.Name = newName
}
func (this Hero) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("Level = ", this.Level)
}
//创建并初始化对象
hero := Hero{Name: "zhangsan", Ad: 100, Level: 1}
要注意,this Hero是调用这个方法的对象的拷贝,因此SetName不会修改原来的属性值,要实现修改还是需要使用传指针
func (this *Hero) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("Level = ", this.Level)
}
func (this *Hero) GetName() string {
return this.Name
}
func (this *Hero) SetName(newName string) {
this.Name = newName
}
前面已经提到了,方法名首字母如果大写,可以被其他包访问。结构体名字,属性首字母如果大写可以被其他包访问,如果小写只有包内部可以访问。这就是go语言的封装。
在SuperMan中Human就表示继承了Human这个类。可以重写方法,添加新方法。
type Human struct {
name string
sex string
}
type SuperMan struct {
Human
level int
}
创建对象:
human := Human{"zhangsan", "female"}
superMan := SuperMan{Human{"lisi", "female"}, 100}
//或者
var s SuperMan
s.name = "zhangsan"
s.sex = "male"
s.level = 3
interface本质是一个指针,在实现类只要重写三个方法就实现了接口。如果重写不完全,接口就不能指向这个实现类。
type AnimalIF interface {
Sleep()
GetColor() string
GetType() string
}
type Cat struct {
color string
}
func (this *Cat) Sleep() {
fmt.Println("Cat is sleeping")
}
func (this *Cat) Getolor() string {
return this.color
}
func (this *Cat) GetType() string {
return "Cat"
}
创建接口指向实现类,需要把对象的地址传过去。
var animal AnimalIF
animal = &Cat{"green"}
animal.Sleep()
animal = &Dog{"blue"}
animal.Sleep()
多态的方法
func showAnimal(animal AnimalIF) {
animal.Sleep()
fmt.Println("color = ", animal.GetColor())
fmt.Println("type = ", animal.GetType())
}
cat := Cat{"black"}
dog := Dog{"yellow"}
showAnimal(&cat)
showAnimal(&dog)
interface{}
,可以用interface{}
引用任意类型。func myFunc(arg interface{}) {
fmt.Println("myFunc is called...")
fmt.Println(arg)
}
book := Book{"golang", "zhangsan"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
func myFunc(arg interface{}) {
value, ok := arg.(string)
fmt.Println(value, ok)
}
var a string
a = "abc"
var allType interface{}
allType = a
str, ok := allType.(string)
fmt.Println(str, ok)
或者
type Reader interface {
ReadBook()
}
type Writer interface {
WriteBook()
}
type Book struct {
}
func (this Book) ReadBook() {
fmt.Println("Read a Book")
}
func (this Book) WriteBook() {
fmt.Println("Write a Book")
}
func main() {
b := Book{}
var r Reader
r = b
r.ReadBook()
var w Writer
w = r.(Writer)
w.WriteBook()
}
上述例子之所以成立是因为“赋值的时候pair不会改变”,复制过去的pair是
。
在reflect包,两个重要API:TypeOf和ValueOf
func main() {
var num float64 = 1.2345
refelctNum(num)
}
func DoFileAndMethod(input interface{}) {
//获取类型
inputType := reflect.TypeOf(input)
fmt.Println("input type is:", inputType.Name())
//获取值
inputValue := reflect.ValueOf(input)
fmt.Println("input value is:", inputValue)
//获取字段Field
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Println(field.Name, field.Type, value)
}
//获取方法并调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Println(m.Name, m.Type)
}
}
注意要用反斜杠,里面是键值对,中间用空格隔开,主要的作用是根据这个标签,判断这个属性在不同包中怎么用。
type resume struct {
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}
func findTag(str interface{}) {
t := reflect.TypeOf(str).Elem()
for i := 0; i < t.NumField(); i++ {
tagString := t.Field(i).Tag.Get("info")
fmt.Println("info:", tagString)
}
}
func main() {
var re resume
findTag(&re)
}
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"price"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}
//编码的过程 结构体-->json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error", err)
return
}
fmt.Printf("jsonStr=%s\n", jsonStr)
//解码的过程 json-->结构体
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Println(myMovie)
}
进程或线程数量越多,切换成本越高,CPU资源越浪费,此外还有高内存占用的弊端。一个线程分为用户态和内核态,划分之后,内核线程称为线程thread,用户线程称为协程co-routine。通过一个内核线程和协程调度器,绑定多个协程。内核线程和协程之间的关系不适合一对多或者一对一,适合N对M。从图中可以看出主要需要优化的就是协程调度器。
Golang的协程内存小,几KB,灵活调度。G表示goroutine协程,P表示处理器,M表示内核线程。
调度器的设计策略:
go语言创建协程非常方便,使用go关键字加上函数即可。
第一种调用,go + 方法
//子Goroutine
func newTask() {
i := 0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
//主Goroutine
func main() {
go newTask()
}
第二种调用,匿名的go协程,匿名方法也可以有形参和返回值,但是返回值不能直接用参数去接,如果要接,其实就是要解决协程之间的通信,这里就要用到channel了
func main() {
//用go创建承载一个形参为空,返回值为空的函数
go func() {
defer fmt.Println("A.defer")
//匿名函数
func() {
defer fmt.Println("B.defer")
runtime.Goexit()//退出当前协程,而不仅是匿名函数
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
time.Sleep(1 * time.Second)
}
}
先定义channel类型变量,然后在协程中通过<-
将值赋给channel变量,最后在主协程中获取channel变量的值。channel有同步两个协程的能力,
func main() {
//定义一个channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine结束")
fmt.Println("goroutine正在运行...")
c <- 666
}()
num := <-c
fmt.Println("num = ", num)
fmt.Println("main goroutine结束...")
}
示意图如下,如果main协程更快执行到了num := <-c
,会被阻塞,等待channel中有值,如果sub协程更快执行,那么main协程能顺利取到管道的值。
对于无缓存的channel,传递消息的一方如果提前到达了要传递channel的指令,但此时接收协程还没有执行到接收channel,那么发送方就需要一直等待,直到接收方来接收。
对于有缓存的channel,发送方将数据发送到channel中便继续执行程序,如果管道中有数据,接收方直接取走数据;发送方发现channel中已经存满了数据时才会被阻塞,接收方直到channel中被取空了才会被阻塞。类似生产者消费者模式。
代码实例:
func main() {
c := make(chan int, 3)
fmt.Println(len(c), cap(c))
go func() {
defer fmt.Println("子goroutine结束...")
for i := 0; i < 3; i++ {
c <- i
fmt.Println("子goroutine正在运行:len(c)=", len(c), "cap(c)=", cap(c))
}
}()
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num=", num)
}
fmt.Println("main goroutine结束")
}
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Main Finished..")
}
这里if后面的语法意思是这样的:先执行data, ok := <-c
,这样data和ok都是if里面的局部变量,然后把ok
作为条件判断是否执行。
类似的,可以用range来获取channel中的数据,把上面的例子修改,代码如下:
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for data := range c {
fmt.Println(data)
}
fmt.Println("Main Finished..")
单个goroutine下只能监控一个channel的状态,select能够实现监控多个channel的状态。
func fibonacii(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x = y
y = x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacii(c, quit)
}
注意:方法传channel传的是指针。
Go modules是Go语言的依赖解决方案,正式于go1.14推荐在生产上使用,目前只要安装了go就安装了go modules。
由于go modules淘汰的是go path,因此需要知道go path工作模式。
Go path弊端:
1、没有版本控制概念
使用go get -u ...
时不能指定版本
2、无法同步一致第三方版本号
别人编写的代码使用的库和我们使用的库版本可能不一致
3、无法指定当前项目引用的第三方版本号
命令 | 功能 |
---|---|
go mod init | 生成go mod文件 |
go mod download | 下载go mod文件中指明的所有依赖 |
go mod vendor | 导出项目所有依赖到vendor目录 |
go mod环境变量通过go env
来查看,重要的环境变量如下:
go env -w GO111MODULE=on
https://proxy.golang.org,direct
,国内上不去,因此需要更换站点:https://mirrors.aliyun.com/goproxy/
https://goproxy.cn,direct
go env -w GOPROXY=https://goproxy.cn,direct
sum.golang.org
,国内访问不了。但只要GOPROXY设置过了,就可以不用设置。go env -w GOPRIVATE="..."
1、保证GO111MODULE是on,具体解释参考上面
go env -w GO111MODULE=on
2、初始化项目
mkdir modules_test
cd modules_test
go mod init github.com/kevin-zkp/modules_test
dir //windows下查看当前目录文件
import (
"fmt"
"github.com/aceld/zinx/ziface"
"github.com/aceld/zinx/znet"
)
//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}
//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
//先读取客户端的数据
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
//再回写ping...ping...ping
err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
func main() {
//1 创建一个server句柄
s := znet.NewServer()
//2 配置路由
s.AddRouter(0, &PingRouter{})
//3 开启服务
s.Serve()
}
//手动download
go get github.com/aceld/zinx/znet
go get github.com/aceld/zinx/ziface
//可以指定版本号,如下
go get github.com/aceld/[email protected]
//自动download
执行go run会自动下载
go.mod文件会多一行如下,如果指定版本,在go.mod中指定
再打开go.sum文件
h1加哈希
使用命令:
go mod edit -replace=版本号1=版本号2
//例如
go mod edit -replace=[email protected]=[email protected]
如果报错:go: -replace=zinx@v1: need old[@v]=new[@w] (missing =)
,
可以尝试改成:go mod edit -replace='[email protected]'='[email protected]'
执行完会在go.mod文件下产生如下一行:
项目源码:
目的是覆盖大部分go语法特性,特别是网络,系统基础结构如下,使用了读写分离模型,使用九个版本迭代:
这里创建main.go和server.go,都是在package main中。在main.go中创建Server并运行。在Server.go中首先创建结构体Server,构造函数,启动函数Start,以及处理业务Handler,每个连接都创建一个Handler协程。
编写之后先运行main.go,再模拟tcp连接,linux下可以nc 127.0.0.1 8888
,windows下telnet 127.0.0.1 8888
,显示如下:
OnlineMap记录上线的用户,使用channel进行广播。
如果使用windows自带的telnet会中文乱码,可以下载putty,选择Other,telnet,输入Ip地址和端口号,点击open,可以看到成功上线。
用户输入一段话,也进行广播。回车换行"\r\n"
。这里如果是windows下,由于telnet一次只能传递一个字符,因此对函数要进行一些改动,需要判断接收的字符是否是回车换行,如果不是则拼接字符串,如果是则进行广播。这里使用了读写分离模式,每个用户都有一个协程接收客户端消息,一个协程广播消息。效果如下:
把能够合并的代码封装成函数,sever.go中的用户上线,下线,广播的代码需要提取到user.go中。需要为User添加属性,表示属于哪个Server。
只要执行time.After就会重置,同时它其实是管道,只要监听管道中能否取到数据即可。把isLive写到上面,这样只要isLive触发,其他case会执行条件,但是不会执行括号内的代码,重置定时器。
这里还是用终端形式,也可以用UI形式。
1、实现连接
windows下编译成可执行文件,linux下去除.exe
即可执行。
go build -o server.exe server.go main.go user.go
go build -o client.exe client.go
2、让客户端指定IP和端口
解析命令行,借助flag库,这样指定.\client.exe -ip 127.0.0.1 -port 8888
func init() {
flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认是127.0.0.1)")
flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认是8888)")
}
3、菜单显示
4、更新用户名
5、公聊模式
6、私聊模式
首先查询当前有哪些用户在线,提示选择一个用户
1、Web框架
beego:国内的框架,文档比较全
gin:国外的轻量级框架,性能较高,比较主流
echo:国外的,轻量级
Iris:国外的,更加重量级,性能较高
2、微服务框架
go kit:包含很多工具,比较灵活
Istio:包括熔断,安全审核等,适合繁琐的大型微服务
3、容器编排
Kubernetes:市场占有率高,谷歌出来的
Swarm:相对不那么高
4、服务发现
类似Java中的zookeeper
consul:服务发现,服务注册
5、存储引擎
k/v存储——etcd:类似Redis,且支持分布式,一致性比较好
分布式存储——tidb:类似MySQL
6、静态建站
hugo
7、中间件
消息队列——nsg
TCP长链接框架(轻量级服务器)——zinx
Leaf(游戏服务器)——Leaf
RPC框架——gRPC
redis集群——codis
8、爬虫框架
go query:效率比python高,但是目前爬虫生态还是python更好