go-入坑笔记

go-入坑笔记.
代码实践仓库: https://github.com/yangxuan0261/GoLab


前篇


规范


go 1.14 发布说明


编辑器 选择 Goland

其他不用考虑了. 参考: go-编辑器GoLand.md


新建一个项目

新建一个文件夹 E:\public_gopath , 用来作为公共的 GOPATH, 设置为 系统环境变量, 所有的 go 项目都可以引用到这里, 就不用重复下载库了


linux 安装 go

  1. 下载安装包: go1.15.3.linux-amd64.tar.gz. 传送门: https://golang.google.cn/dl/

  2. 解压

    1
    2
    3
    $ mv go1.15.3.linux-amd64.tar.gz /opt
    $ cd /opt
    $ tar -xzvf go1.15.3.linux-amd64.tar.gz
  3. 创建 GOPATH 工作区目录, app 代码就放在该目录下

    1
    $ mkdir -p ~/go-workspace/src
  4. 配置环境变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ vim ~/.bash_profile // 添加一下环境变量

    export GOROOT=/opt/go
    export GOPATH=$HOME/go-workspace
    # export GOPROXY=https://goproxy.io
    export GOPROXY=https://goproxy.cn,direct
    export PATH=$PATH:$GOROOT/bin

    $ source ~/.bash_profile // 使之生效

升级 go

  1. 直接删除原来的 go 目录, 将新版本的 go 解压出来即可

    1
    2
    $ rm -fr /opt/go
    $ tar -xzvf go1.15.3.linux-amd64.tar.gz -C /opt
    1. 项目内的模块升级

      1
      2
      3
      4
      5
      6
      7
      8
              $ go mod tidy
      $ go mod vendor

      2. done. 查看版本

      ```json
      $ go version
      go version go1.15.3 linux/amd64

Goland 方式

  1. 新建一个 go app, 文件夹名为 go-lab

  2. 用 goland 直接打开这个 go-lab 文件夹, 就可以开始编写项目了. 默认会使用环境变量中的 GOPATH


docker 方式

只要将 micro 路径挂载进去即可, 然后进入 golang docker 实例中 编译/运行. 参考: docker_golang使用.md


Go 语言风格指南

命名

  • 项目: 用 - 分割单词, 全小写. 如: go-mars (GitHub 上的开源项目都是这样命名)

  • 包名

    • 全部小写。没有大写或下划线。
    • 大多数使用命名导入的情况下,不需要重命名。
    • 简短而简洁。请记住,在每个使用的地方都完整标识了该名称。
    • 不用复数。例如net/url,而不是net/urls
    • 不要用“common”,“util”,“shared”或“lib”。这些是不好的,信息量不足的名称。
  • protobuf 命名

    • package

      全小写, 单词使用 . 分割. 如

      1
      2
      3
      4
      5
      // hello.proto
      package go.micro.srv.greeter;

      // 生成的 hello.pb.go
      package go_micro_srv_greeter
    • message 和 字段 都使用 驼峰 写法, 首字母大写

      1
      2
      3
      4
      5
      6
      7
      message HelloReq {
      string NameById = 1;
      }

      message HelloRsp {
      string NameById = 1;
      }

性能

  • 将原语转换为字符串或从字符串转换时,strconv速度比fmt快。

  • 尽量初始化时指定 Map 容量

    1
    make(map[T1]T2, hint)
  • 相似的声明放在一组, import, var, const 都一样.

    1
    2
    3
    4
    import (
    "a"
    "b"
    )
  • nil 是一个有效的 slice. 零值切片(用var声明的切片)可立即使用,无需调用make()创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var nums []int

    if add1 {
    nums = append(nums, 1)
    }

    func aaa() []int {
    return nil
    }
  • 字符串 string format.

    1
    2
    3
    4
    5
    6
    7
    // bad
    msg := "unexpected values %v, %v\n"
    fmt.Printf(msg, 1, 2)

    // good
    const msg = "unexpected values %v, %v\n" // 这样就不用每次分配 字符串 栈内存
    fmt.Printf(msg, 1, 2)

函数分组与顺序

由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
type something struct{ ... }

func newSomething() *something {
return &something{}
}

func (s *something) Cost() {
return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

其他

  • 使用原始字符串字面值,避免转义, “ ` “ 来表示原生字符串

    1
    2
    wantError := "unknown name:\"test\"" // bad
    wantError := `unknown error:"test"` // goode

优秀开源项目


web 框架

用的比较多, star 数量最多的是 gin: https://github.com/gin-gonic/gin


vscode go.toolsGopath

允许将 go get 下载到的工具隔离到一个 go.toolsGopath 指定的目录中


go get 规则

For example, these commands are all valid:

1
2
3
4
5
go get -d -v github.com/gorilla/mux@latest    # same (@latest is default for 'go get')
go get -d -v github.com/gorilla/mux@v1.6.2 # records v1.6.2
go get -d -v github.com/gorilla/mux@e3702bed2 # records v1.6.2
go get -d -v github.com/gorilla/mux@c856192 # records v0.0.0-20180517173623-c85619274f5d
go get -d -v github.com/gorilla/mux@master # records current meaning of master
  • -d : 只下载不安装
  • -f : 只有在你包含了 -u 参数的时候才有效,不让 -u 去验证 import 中的每一个都已经获取了,这对于本地 fork 的包特别有用
  • -fix : 在获取源码之后先运行 fix,然后再去做其他的事情
  • -t : 同时也下载需要为运行测试所需要的包
  • -u : 强制使用网络去更新包和它的依赖包
  • -v : 显示执行的命令

基础语法教程

菜鸟教程: http://www.runoob.com/go/go-environment.html


常用库


编码规范


构建, 打包, 执行

go build

通过go build加上要编译的Go源文件名,我们即可得到一个可执行文件,默认情况下这个文件的名字为源文件名字去掉.go后缀。

1
2
3
$ go build  hellogo.go
$ ls
hellogo* hellogo.go

当然我们也 可以通过-o选项来指定其他名字 myfirstgo (这个可执行文件):

1
2
3
$ go build -o myfirstgo hellogo.go
$ ls
myfirstgo* hellogo.go
  • 如果是window平台, 则要导出 xxx.exe 可执行文件

    1
    $ go build -o myfirstgo.exe hellogo.go

如果我们在go-examples目录下直接执行go build命令,后面不带文件名,我们将得到一个与目录名同名的可执行文件:

1
2
3
$ go build
$ ls
go-examples* hellogo.go
  • 指定导出 %GOPATH%/src 下某个文件夹下的程序. 该文件加下的必须有 go文件是有 package mainfunc main()

    1
    E:\GoWinEnv>go build -o hello.exe GoLab/test_file
    • 生成的 hello.exe 在执行命令的该文件夹下, 这里就是在 E:\GoWinEnv

构建不同 os 的可执行程序

Golang 支持在一个平台下生成另一个平台可执行程序的交叉编译功能。

  • Mac下编译Linux, Windows平台的64位可执行程序:

    1
    2
    $ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build test.go
    $ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build test.go
  • Linux下编译Mac, Windows平台的64位可执行程序:

    1
    2
    $ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build test.go
    $ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build test.go
  • Windows下编译Mac, Linux平台的64位可执行程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // mac
    $ SET CGO_ENABLED=0
    $ SET GOOS=darwin
    $ SET GOARCH=amd64
    $ go build test.go

    // linux
    $ SET CGO_ENABLED=0
    $ SET GOOS=linux
    $ SET GOARCH=amd64
    $ go build test.go

    GOOS:目标平台的操作系统(darwin、freebsd、linux、windows)
    GOARCH:目标平台的体系架构(386、amd64、arm)
    交叉编译不支持 CGO 所以要禁用它

    上面的命令编译 64 位可执行程序,你当然应该也会使用 386 编译 32 位可执行程序
    很多博客都提到要先增加对其它平台的支持,但是我跳过那一步,上面所列的命令也都能成功,且得到我想要的结果,可见那一步应该是非必须的,或是我所使用的 Go 版本已默认支持所有平台。


go install

与build命令相比,install命令在编译源码后还会将可执行文件或库文件安装到约定的目录下。

  • go install编译出的可执行文件以其所在目录名(DIR)命名
  • go install将可执行文件安装到与src同级别的bin目录下,bin目录由go install自动创建
  • go install将可执行文件依赖的各种package编译后,放在与src同级别的pkg目录下.

go run

直接执行某个 go 文件

1
2
$ cd src\GoLab\test_grpc\greeter_client
$ go run test_grpc_cli.go

参考资料:


面向对象编程


package

  • 同一个目录下不能存在不同包名的文件

  • import 别的包规则

    1
    2
    3
    4
    5
    6
    7
    package main

    import (
    pkg001 "GoLab/test_pkg/pkg001" // 重命名别名为 pkg001 在本文件中的使用, 一般不要这样干
    _ "GoLab/test_pkg/pkg002" // _ 防止 未被使用的包, 被格式化代码时被编辑器自动干掉这一行
    "fmt" // 导入内置包
    )
    • 包名路径: 是 GOPATH/src 为基础搜索.
  • import 的流程. 参考: https://blog.csdn.net/zhangzhebjut/article/details/25564457

    程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:

    1. import

    2. var

    3. init()

    4. main()

    1
    2
    3
    func init() { // 是保留的内置方法, import时自动执行
    fmt.Println("--- init")
    }

空结构体

空结构体的特点:1、不占用内存;2、地址不变

map 的 value, 变相为 set

1
2
empty := struct{}{}
println("empty len:", unsafe.Sizeof(empty)) // 0, 空结构体的长度为 0, 常用于 map 里面做 value 值, 因为 go 里面集合没有 set, 所以用 map 变相做 set

协程的型号量

1
2
3
4
5
6
7
8
9
10
11
12
13
var ch chan struct{}

work := func() {
log.Println("--- work")
time.Sleep(time.Second * 3)
<-ch
}

ch = make(chan struct{}, 10)
for i := 0; i < 15; i++ {
ch <- struct{}{}
go work()
}

Go 关键字和 channel 的用法

make

golang分配内存有一个make函数,该函数第一个参数是类型,第二个参数是分配的空间,第三个参数是预留分配空间. 例如a:=make([]int, 5, 10), len(a)输出结果是5,cap(a)输出结果是10,然后对a[4]进行赋值发现是可以得,但对a[5]进行赋值发现报错了,于是郁闷这个预留分配的空间要怎么使用呢,于是google了一下发现原来预留的空间需要重新切片才可以使用,于是做一下记录,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
func main(){
a := make([]int, 10, 20)
fmt.Printf("%d, %d\n", len(a), cap(a))
fmt.Println(a)
b := a[:cap(a)]
fmt.Println(b)
}
/*
10, 20
[0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
*/

make()分配:内部函数 make(T, args) 的服务目的和 new(T) 不同。
它只生成切片,映射和程道,并返回一个初始化的(不是零)的,type T的,不是 *T 的值。
这种区分的原因是,这三种类型的数据结构必须在使用前初始化.
比如切片是一个三项的描述符,包含数据指针(数组内),长度,和容量;在这些项初始化前,切片为 nil 。
对于切片、映射和程道,make初始化内部数据结构,并准备要用的值。
记住 make() 只用于 映射、切片、程道,不返回指针。要明确的得到指针用 new() 分配。

go 关键字用来创建 goroutine (协程),是实现并发的关键。go 关键字的用法如下:

1
2
3
4
5
6
7
8
9
10
11
//go 关键字放在方法调用前新建一个 goroutine 并让他执行方法体
go GetThingDone(param1, param2);

//上例的变种,新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)

//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
//do someting...
}

因为 goroutine 在多核 cpu 环境下是并行的。如果代码块在多个 goroutine 中执行,我们就实现了代码并行。那么问题来了,怎么拿到并行的结果呢?这就得用 channel 了。

1
2
3
4
5
6
//resultChan 是一个 int 类型的 channel。类似一个信封,里面放的是 int 类型的值。
var resultChan chan int
//将 123 放到这个信封里面,供别人从信封中取用
resultChan <- 123
//从 resultChan 中取值。这个时候 result := 123
result := <- resultChan

channel 详解

chan 是信号的关键字, 作用有点像c++多线程里面的 signal, 发送信号给其他线程, 通知其可以继续往下跑了.
在 go 里 chan 的使用是结合了 go,select 实现了 goroutine, 多并发.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func test_chan03() {
c1 := make(chan string) // 声明 信号
c2 := make(chan string)

go func() { // go 关键字, 新建一个协程跑这个方法
time.Sleep(time.Second * 1)
c1 <- "one" // 往 c1 信号中丢数据, 也就是通知 c1 阻塞的地方可以继续跑了
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()

for i := 0; i < 2; i++ {
select {
case msg1 := <-c1: // 阻塞, 等待 c1 信号通知, 如果收到通知, 这跑这个case, 并把数据丢该 msg1
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
fmt.Println("程序结束666")
/*
msg1 := <-c1 表示 阻塞, 等待c1信号通知, 收到通知后把数据 赋值给 msg1. 如果不需要信号中的数据, 可以可以这样写 <-c1

c1 <- "one" 表示 通知 c1 信号阻塞的地方可以继续运行了, 并往里面丢了一个数据 "one"
*/
}

channel 读写加持

chan 在函数中定义形参是时可以指定是 读写,只读,只写 三个形式, 作用与 c++ 中 const关键字 差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fnRW := func(c chan int) { // c可以读写
c <- 6
val := <-c
fmt.Println("val:", val)
}

fnR := func(c <-chan int) { // c只读
// c <- 6 // 报错: send to receive-only type <-chan int
val := <-c
fmt.Println("val:", val)
}

fnW := func(c chan<- int) { // c只写
// <-c // 报错: receive from send-only type chan<- int
c <- 6
}

参考: https://www.cnblogs.com/baiyuxiong/p/4545028.html

channel 赋值不生效

可能没有初始化变量 make(chan string)


defer

defer 的思想类似于C++中的析构函数,不过Go语言中“析构”的不是对象,而是函数,defer就是用来添加函数结束时执行的语句。注意这里强调的是添加,而不是指定,因为不同于C++中的析构函数是静态的,Go中的defer是动态的
defer 中使用匿名函数依然是一个闭包。

1
2
3
4
5
6
7
8
9
func test_defer() {
x, y := 1, 2
defer func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y) // y 为闭包引用
}(x) // 复制 x 的值
x += 100
y += 100
fmt.Println(x, y)
}

defer 还有一个重要的作用是用于 panic 时的 恢复, panic 恢复也只能在 defer 中.
参考: http://wiki.jikexueyuan.com/project/the-way-to-go/13.3.html

参考: 5 年 Gopher 都不知道的 defer 细节,你别再掉进坑里

什么是 defer

defer 是 Go 语言提供的一种用于注册延迟调用的机制,每一次 defer 都会把函数压入栈中,当前函数返回前再把延迟函数取出并执行。

defer 语句并不会马上执行,而是会进入一个栈,函数 return 前,会按先进后出(FILO)的顺序执行。也就是说最先被定义的 defer 语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行,那后面函数的依赖就没有了

采坑点

使用 defer 最容易采坑的地方是和带命名返回参数的函数一起使用时。

defer 语句定义时,对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用。作为函数参数,则在 defer 定义时就把值传递给 defer,并被缓存起来;作为闭包引用的话,则会在 defer 函数真正调用时根据整个上下文确定当前的值。

避免掉坑的关键是要理解这条语句:

1
return xxx

这条语句并不是一个原子指令,经过编译之后,变成了三条指令:

1
2
3
1.返回值=xxx
2.调用defer函数
3.空的return

1,3 步才是 return 语句真正的命令,第 2 步是 defer 定义的语句,这里就有可能会操作返回值


性能

补充上柴大的回复:“不是性能问题,defer 最大的功能是 Panic 后依然有效。如果没有 defer,Panic 后就会导致 unlock 丢失,从而导致死锁了”,非常经典。

结论, 性能影响小, 但对于 调用极多 的函数, 能不用就不用.


new make 区别

  • new

    new 这个内置函数,可以给我们分配一块内存让我们使用,但是现实的编码中,它是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如: u:=&user{}

  • make

    make 函数是无可替代的,我们在使用 slice、map 以及 channel 的时候,还是要使用 make 进行初始化,然后才才可以对他们进行操作。

make 返回的还是这三个引用类型本身;而 new 返回的是指向类型的指针。


slice

slice 是容易踩坑的地方

如果要从某个 slice 中切除部分来 appen 新内容, 一定要使用 copy 的方式拷贝出来新的, 不然极其可能会修改到原来 slice 的数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arr1 := []int{1, 2, 3}

// 正确姿势
arr2 := make([]int, len(arr1))
copy(arr2, arr1)
arr2 = append(arr2, 4)

fmt.Printf("--- arr1: %+v\n", arr1) // [1 2 3]
fmt.Printf("--- arr2: %+v\n", arr2) // [1 2 3 4]

// 错误姿势
arr5 := arr1[1:2]
arr5 = append(arr5, 4)
fmt.Printf("--- arr1: %+v\n", arr1) // [1 2 4], arr1 被修改了
fmt.Printf("--- arr5: %+v\n", arr5) // [2 4]

CGO

CGO 提供了 golang 和 C 语言相互调用的机制。某些第三方库可能只有 C/C++ 的实现,完全用纯 golang 的实现可能工程浩大,这时候 CGO 就派上用场了。可以通 CGO 在 golang 在调用 C 的接口,C++ 的接口可以用 C 包装一下提供给 golang 调用。被调用的 C 代码可以直接以源代码形式提供或者打包静态库或动态库在编译时链接。推荐使用静态库的方式,这样方便代码隔离,编译的二进制也没有动态库依赖方便发布也符合 golang 的哲学。

goroutine 通过 CGO 进入到 C 接口的执行阶段后,已经脱离了 golang 运行时的调度并且会独占线程,此时实际上变成了多线程同步的编程模型。如果 C 接口里有阻塞操作,这时候可能会导致所有线程都处于阻塞状态,其他 goroutine 没有机会得到调度,最终导致整个系统的性能大大较低。总的来说,只有在第三方库没有 golang 的实现并且实现起来成本比较高的情况下才需要考虑使用 CGO ,否则慎用。


热重启


Go 错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:

1
2
3
type error interface {
Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

1
2
3
4
5
6
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}

在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

1
2
3
4
5
result, err:= Sqrt(-1)

if err != nil {
fmt.Println(err)
}

select 语句的行为

1
2
3
4
5
6
7
8
9
10
11
// https://talks.golang.org/2012/concurrency.slide#32
select {
case v1 := <-c1:
fmt.Printf("received %v from c1\n", v1)
case v2 := <-c2:
fmt.Printf("received %v from c2\n", v1)
case c3 <- 23:
fmt.Printf("sent %v to c3\n", 23)
default:
fmt.Printf("no one was ready to communicate\n")
}

上面这段代码中,select 语句有四个 case 子语句,前两个是 receive 操作,第三个是 send 操作,最后一个是默认操作。代码执行到 select 时,case 语句会按照源代码的顺序被评估,且只评估一次,评估的结果会出现下面这几种情况:

  1. 除 default 外,如果只有一个 case 语句评估通过,那么就执行这个case里的语句;
  2. 除 default 外,如果有多个 case 语句评估通过,那么通过伪随机的方式随机选一个;
  3. 如果 default 外的 case 语句都没有通过评估,那么执行 default 里的语句;
  4. 如果没有 default,那么 代码块会被阻塞,指导有一个 case 通过评估;否则一直阻塞

以下描述了 select 语句的语法:

  • 每个case都必须是一个通信

  • 所有channel表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果任意某个通信可以进行,它就执行;其他被忽略。

  • 如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。

    否则:

    • 如果有default子句,则执行该语句。
    • 如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

接口 interface

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
/* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}

什么时候需要指针 *

当使用 interface 时不需要 *, 只有 struct 时才需要 *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type CDog struct {
}

func (this *CDog) Run() { // 实现 IDog 的所有接口
fmt.Println("--- run")
}

type IDog interface{
Run()
}

var ptr1 interface{}
ptr1 = &CDog{} // interface 变量接收的是指针, 而不是对象
if myDog, ok := ptr1.(*CDog); ok { // 动态匹配 CDog 指针
fmt.Printf("--- ptr1 is CDog \n") // --- ptr1 is CDog
}

if myDog, ok := ptr1.(IDog); ok { // 动态匹配 IDog 接口, 不需要指针符号 *
fmt.Printf("--- ptr1 is IDog \n") // --- ptr1 is CDog
}

context

一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

一个是TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

  • 创建 ctx 的四个接口

    1
    2
    3
    4
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    func WithValue(parent Context, key, val interface{}) Context

    这四个With函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。

    通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

    • WithCancel 函数,传递一个父 Context作为参数,返回子Context,以及一个取消函数用来取消Context。 WithDeadline 函数,和 WithCancel 差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消 Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

    • WithTimeoutWithDeadline基本上一样,一个是 多少时间后 超时自动取消,一个是 什么时间点 超时自动取消。

    • WithValue 函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的 Context,这个绑定的数据可以通过 Context.Value 方法访问到


TLS


模块

自从 Go 官方从去年推出 1.11 之后,增加新的依赖管理模块并且更加易于管理项目中所需要的模块。模块是存储在文件树中的 Go 包的集合,其根目录中包含 go.mod 文件。 go.mod 文件定义了模块的模块路径,它也是用于根目录的导入路径,以及它的依赖性要求。每个依赖性要求都被写为模块路径和特定语义版本。

从 Go 1.11 开始,Go 允许在 $GOPATH/src 外的任何目录下使用 go.mod 创建项目。在 $GOPATH/src 中,为了兼容性,Go 命令仍然在旧的 GOPATH 模式下运行。从 Go 1.13 开始,模块模式将成为默认模式。

  1. 环境变量设置

    1
    2
    export GO113MODULE=on // 113 是对应 go 的版本 1.13
    export GOPROXY=https://goproxy.io // 设置代理 // 可以翻墙的话就不用设置这个代理了

    如果不生效, 使用这个. 参考: https://www.jianshu.com/p/e0c878d4ca19

    1
    go env -w GOPROXY=https://goproxy.cn,direct
  2. 初始化新模块.

    1. $GOPATH/src 之外的任何地方创建一个新的目录

      1
      2
      F:\a_link_workspace\go\GoWinEnv_Test01\src (master -> origin)
      λ mkdir backend && cd backend
    2. 模块初始化. 命令: go mod init xxx. xxx 为模块名 ( 模块名最好和仓库根目录路径一致 ) . 会生成一个模块配置文件 go.mod

      1
      2
      3
      F:\a_link_workspace\go\GoWinEnv_Test01\src\backend (master -> origin)
      λ go mod init backend
      go: creating new go.mod: module backend
      • 查看内容

        1
        2
        3
        4
        5
        F:\a_link_workspace\go\GoWinEnv_Test01\src\backend (master -> origin)
        λ cat go.mod
        module backend

        go 1.13
  3. 测试. 新建一个 main.go, 引入一个第三方模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main

    import "github.com/gin-gonic/gin" // 引入的第三方模块

    func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
    c.JSON(200, gin.H{
    "message": "pong",
    })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
    }
    • run 一下

      1
      2
      F:\a_link_workspace\go\GoWinEnv_Test01\src\backend (master -> origin)
      λ go run main.go

      go.mod 会自动引入的第三方模块,

      1
      2
      3
      4
      5
      6
      7
      8
      module backend

      go 1.13

      require (
      github.com/gin-gonic/gin v1.4.0
      github.com/labstack/echo v3.3.10+incompatible // indirect
      )

命令

  • go mod download 下载模块到本地缓存,缓存路径是 $GOPATH/pkg/mod/cache

  • go mod edit 是提供了命令版编辑 go.mod 的功能,例如 go mod edit -fmt go.mod 会格式化 go.mod

  • go mod graph 把模块之间的依赖图显示出来

  • go mod init 初始化模块(例如把原本dep管理的依赖关系转换过来)

  • go mod tidy 增加缺失的包,移除没用的包

  • go mod vendor 把依赖拷贝到 vendor/ 目录下

    修改 vendor 第三方库使其生效. 参考: 使用本地包

    把第三方库指到 vendor 目录下对应的库即可

  • go mod verify 确认依赖关系

  • go mod why 解释为什么需要包和模块


使用本地包

有些情况下需要修改第三方库进行 调试/打log, 不引用远程包

直接修改 go.mod 文件, 修改 go-plugins 包的指向

1
2
3
4
5
6
require (
github.com/micro/go-plugins v1.3.0
...
)

replace github.com/micro/go-plugins => ../github.com/micro/go-plugins // 修改为本地包, 可以使用相对于 go.mod 的相对路径, 也可以使用绝对路径

然后修改本的 go-plugins 中的代码就可以编译进去了.

使用 本地包 有个坑. 因为 mod 模式下引用的包都是按需下载, 所以下载的第三方库并不是所在完整的库. 一旦代码引用到这个库里面的其他 package 时, 就会报找不到的错误.

1
go: github.com/micro/go-micro@v1.11.1: parsing ..\..\..\..\vendor\github.com\micro\go-micro\go.mod: open f:\a_link_workspace\go\GoWinEnv_new\src\vendor\github.com\micro\go-micro\go.mod: The system cannot find the path specified.

解决办法是 取消 掉 go.mod 中 指向本地包的代码

1
// replace github.com/micro/go-micro => ../vendor/github.com/micro/go-micro

然后在 go run 一下, 就下载, 再 go mod vendor 一下会移到 vendor 目录下

1
2
3
f:\a_link_workspace\go\GoWinEnv_new\src\GoMicro\test_cmd\test_cmd_service\cmd1 (master -> origin)
$ go run main.go
$ go mod vendor
正确的姿势
  1. 仓库根目录下, 添加一个 子模块, 将 完整的包 下下来丢到 src 目录下

    • 如果不需要提交这个 子模块, 在 .gitmodules 中删掉这个 子模块
  2. 修改 go.mod 中仓库的 引用包 的指向

    1
    replace github.com/micro/go-micro => ../github.com/micro/go-micro
  3. 这样修改 ../github.com/micro/go-micro 目录下的代码就能编译进去生效了.


检查 项目是否有错误

  • 直接在根目录使用命令: go mod vendor

使用其他版本的包

1
2
3
4
5
replace github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9 => github.com/gogo/protobuf v0.0.0-20190723190241-65acae22fc9d // 指向其他版本的包

require (
github.com/golang/protobuf v1.3.2
)

发布版本 - 打 tag


导入所有依赖

使用命令 go get -d -v ./...

  • -d : 标志只下载代码包,不执行安装命令;
  • -v : 打印详细日志和调试日志。这里加上这个标志会把每个下载的包都打印出来;
  • ./... : 这个表示路径,代表当前目录下所有的文件。

docker go环境

官网地址: https://hub.docker.com/_/golang/

容器内默认工作区是 /go , 所以可以挂载到 /go/src 目录下

常见问题

  • ssh 远程连进去, 不知道为啥 没有go指令, 需要自己添加到环境变量中 .bash_profile

    1
    2
    3
    export PATH=$PATH:/usr/local/go/bin
    GO_BIN=/usr/local/go/bin
    export GO_BIN

    然后使其生效 # source .bash_profile


ubuntu 安装 go

  1. 下载 go1.10.3.linux-amd64.tar , 地址:https://golang.google.cn/dl/

  2. 解压: # tar zxvf go1.10.3.linux-amd64.tar.gz -C /usr/local

  3. 增加环境变量

    1
    2
    3
    4
    5
    6
    7
    # vi ~/.bash_profile
    ...
    export GOROOT=/usr/local/go
    export GOPATH=/mytemp/GoLab # 项目地址
    export PATH=$PATH:$GOPATH:/usr/local/go/bin

    # source ~/.bash_profile # 使其生效
  4. 查看命名, ok

    1
    2
    # go version
    go version go1.10.3 linux/amd64

唯一ID生成


RabbitMQ


反射 reflect

优化, 可以通过 自定义的生成器脚本, 生成, 避免使用反射


Builder & Option 设计模式


服务的 ip 和 端口

如果要让 端口 给外部连接, 一定要写成 :8080 的形式, 不要使用 127.0.0.1:8080, 因为这样指定了只有本机可以连接

正确姿势:

1
2
addr := ":6601"
listener, err := net.Listen("tcp", addr)

GC


并发模型


数据竞争

很多处在竞态的错误很难发现,Go 语言中提供了一个工具,可以帮忙检查代码中是否存在竞态。使用起来很简单,只需要在以下命令之后加上 -race 参数就可以:

1
2
3
$ go run -race
$ go build -race
$ go test -race

加上这个参数之后,编译器会对代码在执行时对所有共享变量的访问,如果发现一个 goroutine 写入一个变量之后,没有任何同步的操作,就有另外一个 goroutine 读写了这个变量,那就说明这里存在竞态,就会报错。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
data := 1

go func() {
data = 2
}()

go func() {
data = 3
}()

time.Sleep(2 * time.Second)

运行 go run -race main.go 之后,会报下面的错误:

1
2
Found 1 data race(s)
exit status 66

文档/注释


方法

  1. 可以 一行 或者 两行

    1
    2
    3
    4
    5
    // Add 两数相加(这一行会被截取为简短介绍, [方法名 descr] )
    // 两数相加的注意事项以及原理(这一行作为超级详细的介绍)
    func Add(n1,n2 int)int{
    return n1+n2
    }

废弃

  1. 接口加上 Deprecated 注释

    1
    2
    // Deprecated: Do not use.
    func NewRpc(opts ...Option) *Rpc {
  2. 编辑器上就可以提示出来


系统标记

参考:

  1. 排除 windows 系统

    1
    2
    3
    4
    // +build !windows

    package platform
    ...
  2. 编辑器上就可以提示出来


generate

  1. 生成提示

    1
    2
    3
    package net // import "v2ray.com/core/common/net"

    //go:generate go run v2ray.com/core/common/errors/errorgen
  2. 要执行的代码 v2ray.com/core/common/errors/errorgen/main.go

    1
    2
    3
    package main

    func main() {}
  3. 编辑器上 alt + enter 就可以提示出来


常见编译报错

  • Q: can’t load package: package test: found packages main (base.go) and testgo (test_go.go) in E:\GoLab\src\test

    A: 同一个目录下不能存在不同包名的文件

declared and not used

声明但未被使用, 可以这样屏蔽报错

1
2
xf := xiaofang{}
_ = xf

源码阅读

Context 源码剖析


time/rate 实现剖析


踩坑

保存自动格式化, 源代码被自动删除

参考:

https://github.com/microsoft/vscode-go/issues/2604

https://stackoverflow.com/questions/48124565/why-does-vscode-delete-golang-source-on-save

把格式化工具由 goreturns 换成 goformat, 同时取消掉保存自动格式化

1
2
3
4
5
6
7
"go.formatTool": "gofmt", // 使用 gofmt 工具格式化
"go.alternateTools": {
"go-langserver": "gopls"
},
"[go]": {
"editor.formatOnSave": false
},

go get 报错: ‘xxx’ is not using a known version control system

可能有两个原因

  1. 删除本地的 xxx 目录
  2. 没有连上 vpn

go run 报错: The system cannot find the path specified.

可能 go.mod 把某个包指向了本地 vendor 里面的包, 而 vendor 里面又没有这个包.

解决办法参考: 使用本地包

报错: all goroutines are asleep - deadlock!

main goroutine 在等一个永远不会来的数据,那整个程序就永远等下去了, 这个时候就会报上述错误

需要用 sync.WaitGroup 来保证程序正常退出. 参考: test_error.go

参考:

https://stackoverflow.com/questions/26927479/go-language-fatal-error-all-goroutines-are-asleep-deadlock

https://cloud.tencent.com/developer/article/1418106

map 多线程访问加锁

即使是只是访问, 不做 add 或 delete 操作, 都得加锁, 不然会有潜在的错误:

1
runtime.mapaccess2_fast64(0x77e360, 0xc0000da000, 0xab1, 0x86f1e0, 0xc001325040)

多线程访问的正确姿势 - 加锁

1
2
3
4
5
a.callMu.Lock()
if ci, ok := a.callMap[pbData.Header.ReqID]; ok {
ci.ch <- pbData
}
a.callMu.Unlock()

channel 导致死锁问题

  1. goroutine 3609 [chan receive]:
  2. goroutine 3609 [chan send]:
  3. goroutine 8 [running]:

等问题, 要不是 chan 没被消费, 就是被过度消费. 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://juejin.im/post/5ca318e651882543db10d4ce
// 测试死锁
func Test_goroutinueDeadLock(t *testing.T) {
ch := make(chan int)
ch <- 5
}

/*
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
testing.(*T).Run(0xc0000a8100, 0x5615e7, 0x17, 0x567ba0, 0x47c601)

goroutine 6 [chan send]:
GoLab/test_channel.Test_goroutinueDeadLock(0xc0000a8100)
F:/a_link_workspace/go/GoWinEnv_new/src/GoLab/test_channel/channel_test.go:228 ch <- 5
*/

编译报错: can’t load package: package xxx: malformed module path “xxx”: missing dot in first path element

模块名与 目录名 不一致, 修改为一致即可, 如: go.mod 中的 module mars 修改为 module go_mars


go get 报错: use of internal package ‘xxx’ not allowed

很简单,vendor 文件夹里面的包路径出现计算机多个目录下,例如c:\go\src;d:\myapp\src等文件夹下存在相同的路径,编译器无法决定加载哪个路径下的文件,于是报错. 删掉 对应的 vendor 目录即可.

参考: https://blog.csdn.net/quicmous/article/details/81292628


for range 遍历 取地址的坑

for range 遍历时, 不能去取 value 值的地址, 因为这个 value 并不是 复制拷贝出来的对象

1
2
3
4
5
6
7
8
9
slice := []int{0, 1, 2, 3}
myMap := make(map[int]*int)

for index , value := range slice {
// myMap[index] = &value // 错误姿势

tmp := value // 正确姿势, 使用复制拷贝出来的对象取地址, 才是正确
myMap[index] = &tmp
}

参考: https://studygolang.com/articles/14176


todo:


bottom