Go_入坑笔记


title: go-入坑笔记
categories: Go
tags: [go, 编程]
date: 2018-08-02 20:16:18
comments: false

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


前篇

  • Go 入门指南 - http://wiki.jikexueyuan.com/project/the-way-to-go/
  • Go 语言圣经 - https://books.studygolang.com/gopl-zh/ch0/ch0-01.html
  • 对pkg里面的针对每个函数写代码例子 - https://github.com/astaxie/gopkg
  • 通用教程 - https://github.com/astaxie/build-web-application-with-golang
  • 由一个经验丰富的Go程序员群体编写的一系列Go学习范例 - https://github.com/mkaz/working-with-go/tree/master/pages
  • Go社区投票选举出来的最好的在线 Go 教程 - https://hackr.io/tutorials/learn-golang
  • Go by Example - https://gobyexample.com/
  • 教程清单 - https://github.com/yinggaozhen/awesome-go-cn

环境配置 (废弃, 使用 Goland 编码体验更好)


windows vscode 配置 (2019.10.10)

  1. 弄好代理, Proxifier 代理到 ssr. 这样命令行才能走到 代理上

  2. ctrl + shift + p, 输入 go install, 选择 go: install/update tools, 然后全选, 点击ok

    Go_入坑笔记_第1张图片

    只要代理弄好了, 全部都能安装成功. 生成的 一些列 exe 会在 bin 目录下

    Installing 1 tool at F:\a_link_workspace\go\GoWinEnv_MicroExamples\bin
      goimports
      ...
    
    Installing golang.org/x/tools/cmd/goimports SUCCEEDED
    ...
    
    All tools successfully installed. You're ready to Go :).
    
  3. src 下的项目用 模块管理. 参考: [模块依赖 go mod](#模块依赖 go mod)


vscode 模块化支持

参考: Go modules support in Visual Studio Code - https://github.com/Microsoft/vscode-go/wiki/Go-modules-support-in-Visual-Studio-Code


工程模板

  • https://github.com/yangxuan0261/GoWinEnv_Template

新建一个项目

Go_入坑笔记_第2张图片

micro 文件夹下的才是项目的源码, 其他文件夹都是依赖的第三方库. 所以 micro 才是 git 仓库.

  • windows 上使用 Goland 打开 GoEnvMicro 作为工程
  • Linux 中使用 docker 的话, 只要将 micro 路径挂载进去

Goland 方式

Go_入坑笔记_第3张图片

  1. 新建一个 go 项目目录 GoEnvMicro, 再建一个子目录 src, src 目录下的 micro 才是真正的项目源码, 这个才需要用 git 版本控制的代码, GitHub 里的 go 开源项目就是这个. (pkg 目录会自动生成)

  2. 用 Goland 打开 GoEnvMicro 这个目录作为项目

    Go_入坑笔记_第4张图片

  3. 模块化这个项目源码目录, cd 到 micro 目录下使用命令: go mod initxxx

    E:\ws_go\GoEnvMicro\src\micro
    $ go mod init micro
    go: creating new go.mod: module micro
    
  4. 此时就可以 run 和 build项目了, 但是 Goland 中提示会报错, 且不能跳转到 项目内/第三方库 的符号.

    1. 项目条状符号, 添加项目 GOPATH (最好除一下系统的 GOPATH)

      Go_入坑笔记_第5张图片

      import "micro/tool" // 就不会报错了
      
    2. alt + enter 快捷键 go get 第三方库

      Go_入坑笔记_第6张图片

    然后就可以跳转到 项目内/第三方库 的符号了


docker 方式

docker 方式就简单很多

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


Go 语言风格指南

  • https://github.com/uber-go/guide/blob/master/style.md

优秀开源项目

  • https://zhuanlan.zhihu.com/p/50413709
  • https://github.com/hackstoic/golang-open-source-projects
  • https://github.com/yinggaozhen/awesome-go-cn (汇总了多个开源项目)

vscode go.toolsGopath

  • https://github.com/Microsoft/vscode-go/wiki/GOPATH-in-the-VS-Code-Go-extension

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


go get 规则

  • https://tip.golang.org/cmd/go/#hdr-Module_queries

For example, these commands are all valid:

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 : 标志只下载代码包,不执行安装命令;
  • -v : 打印详细日志和调试日志。这里加上这个标志会把每个下载的包都打印出来;

基础语法教程

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


golang 多核调度

参考:

  • 对golang多核编程的一点了解 - https://studygolang.com/articles/9686
  • Go语言中的多核调度 - https://blog.csdn.net/InsZVA/article/details/54081605
  • Golang 的 协程调度机制 与 GOMAXPROCS 性能调优 - https://juejin.im/post/5b7678f451882533110e8948

GOMAXPROCS

在 Go语言程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的,Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为:

runtime.GOMAXPROCS(逻辑CPU数量)

这里的逻辑CPU数量可以有如下几种数值:

  • <1:不修改任何数值。
  • =1:单核心执行。
  • >1:多核并发执行。

一般情况下,可以使用 runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置,例如:

runtime.GOMAXPROCS(runtime.NumCPU())

Go 1.5 版本之前,默认使用的是单核心执行。从 Go 1.5 版本开始,默认执行上面语句以便让代码并发执行,最大效率地利用 CPU。

GOMAXPROCS 同时也是一个环境变量,在应用程序启动前设置环境变量也可以起到相同的作用。


常用库

  • 官网包 - https://godoc.org/
  • Golang常用包有哪些?- https://www.zhihu.com/question/22009370

编码规范

  • https://gocn.vip/article/1

构建, 打包, 执行

go build

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

$ go build  hellogo.go
$ ls
hellogo* hellogo.go

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

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

    $ go build -o myfirstgo.exe hellogo.go
    

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

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

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

go install

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

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

go run

直接执行某个 go 文件

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

参考资料:

  • http://tonybai.com/2012/08/17/hello-go/

面向对象编程

  • https://blog.csdn.net/zhangjg_blog/article/details/18790965

package

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

  • import 别的包规则

    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()

      Go_入坑笔记_第7张图片

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

空结构体

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

map 的 value, 变相为 set

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

协程的型号量

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了一下发现原来预留的空间需要重新切片才可以使用,于是做一下记录,代码如下。

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 关键字的用法如下:

//go 关键字放在方法调用前新建一个 goroutine 并让他执行方法体
go GetThingDone(param1, param2);

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

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

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

//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, 多并发.

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关键字 差不多

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 中使用匿名函数依然是一个闭包。

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 函数真正调用时根据整个上下文确定当前的值。

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

return xxx

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

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

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


性能

  • Go defer 会有性能损耗,尽量不要用?- https://segmentfault.com/a/1190000019490834

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

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


new make 区别

  • new

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

  • make

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

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


CGO

  • https://studygolang.com/articles/16812

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

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


热重启

  • Golang中的热重启 - https://cloud.tencent.com/developer/article/1388556

Go 错误处理

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

type error interface {
    Error() string
}

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

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方法)被调用,以输出错误,请看下面调用的示例代码:

result, err:= Sqrt(-1)

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

select 语句的行为

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

/* 定义接口 */
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 时才需要 *

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

  • Go语言实战笔记(二十)| Go Context - https://www.flysnow.org/2017/05/12/go-in-action-go-context.html
  • https://juejin.im/post/5a6873fef265da3e317e55b6

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

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

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

  • 创建 ctx 的四个接口

    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 方法访问到


protobuf 使用

proto3

  • https://juejin.im/post/5bb597c2e51d450e6e03e42d

参考: https://blog.csdn.net/wangshubo1989/article/details/72478014

报错: 语法没有指定
-> 在 test.proto 指定语法

syntax = "proto2";
package goprotobuf;

报错: --go_out: protoc-gen-go: 系统找不到指定的文件
-> 需要将 go get -u github.com/golang/protobuf/protoc-gen-go 下载的 protoc-gen-go.exe 所在目录加入环境变量

安装

  1. 下载 protoc. https://github.com/protocolbuffers/protobuf/releases

  2. 下载并生成 protoc-gen-go.exe, protoc-gen-micro.exe 等 Golang for protobuf 插件

    $ go get -u -v github.com/golang/protobuf/proto
    $ go get -u -v github.com/golang/protobuf/protoc-gen-go
    

生成 grpc xxx.pb.go

得先安装 grpc. 参考: grpc

  • 执行命令所在路径为 test_grpc

    ├─test_grpc
    │  ├─aaa
    │  ├─greeter_client
    │  ├─greeter_server
    │  └─protos // 所有 .proto 文件
    

    执行命令

    protoc --go_out=plugins=grpc:aaa -I .\protos ./protos/helloworld.proto
    
    • -I .\protos : 指定 依赖路径, 因为 proto 文件在同一文件夹内必须是相同 package (相同模块), 如果依赖了其他模块需要指定 -I 参数指定搜索路径

      • 奇怪的是带上 -I 参数, 则会在输出目录 aaa 下直接生成 xxx.pb.go, 不带 -I 参数则会生成多一级目录, 也就是 aaa/protos/xxx.pb.go
    • .\protos\helloworld.proto : 指定要生成的文件为 protos\helloworld.proto 文件

      • 如果该模块有多个 proto 文件需要转换的话, 可以使用 ./protos/*.proto, 会匹配生成多个 xxx.pb.go 文件
      • 也可以分别制定多个 proto 文件, 用空格隔开. ./protos/aaa.proto ./protos/bbb.proto
    • --go_out=plugins=grpc:aaa : 输出文件 到 aaa (必须已经存在) 目录下, 格式为 go 语言的 grpc 格式

      • 如果生成普通的 xxx.pb.go 的话, 使用 protoc-gen-go, 则为: --go_out=protoc-gen-go:bbb
    • 结果: 会在 aaa 文件夹下生成 xxx.pb.go 文件

      Go_入坑笔记_第8张图片

生成 普通 xxx.pb.go

  • 执行命令. ( protoc-gen-go.exe 已经在环境变量中, 且输出目录 bbb 已经存在 )

    protoc -I .\protos\ --go_out=protoc-gen-go:bbb ./protos/helloworld.proto
    

插件 gofast, 高性能

  • golang使用protobuf - https://segmentfault.com/a/1190000009277748

gogoprotobuf有两个插件可以使用

  • protoc-gen-gogo:和protoc-gen-go生成的文件差不多,性能也几乎一样(稍微快一点点)
  • protoc-gen-gofast:生成的文件更复杂,性能也更高(快5-7倍)
//gogo
$ go get github.com/gogo/protobuf/protoc-gen-gogo
$ protoc --gogo_out=. *.proto

//gofast
$ go get github.com/gogo/protobuf/protoc-gen-gofast
$ protoc --gofast_out=. *.proto

proto2 语法生成字段都是指针

  • https://github.com/golang/protobuf/issues/22

grpc

  • grpc应用详解 - https://www.jishuwen.com/d/2BV4
  • Go实践微服务 – gRPC配置和使用 - https://yuanxuxu.com/2018/06/20/go-microservice-in-action-grpc/
  • gRPC 基础: Go - https://doc.oschina.net/grpc?t=60133
  • https://github.com/micro/development/blob/master/clients.md

使用的 proto 的语法必须是3. ( syntax = "proto3"; )


安装

go get -u -v google.golang.org/grpc

生成包含 grpc 的 pb

  • Go实战–golang中使用gRPC和Protobuf实现高性能api(golang/protobuf、google.golang.org/grpc) - https://blog.csdn.net/wangshubo1989/article/details/78739994
$ protoc --go_out=plugins=grpc:. your_pb.proto

单向调用

也就是 请求->响应 式的调用.

参考: grpc_call

// proto
syntax = "proto3";

package helloworld;

service Greeter{
    rpc SayHello (HelloRequest) returns (HelloReply){}
}

message HelloRequest{
    string name = 1;
}

message HelloReply{
    string message = 1;
}

// go
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	fmt.Printf("--- from cli, say:%s\n", in.Name)
	return &pb.HelloReply{Message: "hello " + in.Name}, nil
}

双向调用

双方都可主动推送流过去

参考: grpc_stream_putget

// proto
syntax = "proto3";//声明proto的版本 只能 是3,才支持 grpc

package pro;

service Greeter {
  rpc AllStream (stream StreamReqData) returns (stream StreamResData){}
}

message StreamReqData {
   string data = 1;
}

message StreamResData {
   string data = 1;
}

// go
//客户端服务端 双向流, req res 都定义了 stream, 必须通过 recv 和 send 接收发送数据, 然后再通过 .xxx 获取属性值
func (this *server) AllStream(allStr pro.Greeter_AllStreamServer) error {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		for {
			data, _ := allStr.Recv()
			log.Println("--- srv AllStream recv:", data.Data)
		}
		wg.Done()
	}()

	go func() {
		for {
			allStr.Send(&pro.StreamResData{Data: "--- srv data"})
			time.Sleep(time.Second)
		}
		wg.Done()
	}()

	wg.Wait()
	return nil
}

TLS

  • 带入gRPC:基于 CA 的 TLS 证书认证 - https://studygolang.com/articles/15331

模块依赖 go mod

  • Go依赖模块版本之Module避坑使用详解 - https://www.cnblogs.com/sunsky303/p/10710637.html
  • go mod 使用 - https://juejin.im/post/5c8e503a6fb9a070d878184a
  • Go Modules 详解使用 - https://learnku.com/articles/27401
  • Golang Modules ( 建议, 详细 ) - https://leileiluoluo.com/posts/golang-modules.html

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

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

  1. 环境变量设置

    export GO113MODULE=on // 113 是对应 go 的版本 1.13
    export GOPROXY=https://goproxy.io // 设置代理 // 可以的话就不用设置这个代理了
    
  2. 初始化新模块.

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

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

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

        F:\a_link_workspace\go\GoWinEnv_Test01\src\backend (master -> origin)
        λ cat go.mod
        module backend
        
        go 1.13
        
  3. 测试. 新建一个 main.go, 引入一个第三方模块

    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 一下

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

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

      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 解释为什么需要包和模块


使用本地包

  • go module使用本地包 - https://segmentfault.com/a/1190000018672890

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

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

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 时, 就会报找不到的错误.

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 中 指向本地包的代码

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

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

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 目录下

    Go_入坑笔记_第9张图片

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

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


使用其他版本的包

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
)

导入所有依赖

  • https://www.cnblogs.com/landv/p/10948227.html

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

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

docker go环境

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

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

常见问题

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

    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. 增加环境变量

    # 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

    # go version
    go version go1.10.3 linux/amd64
    
  • 参考: https://blog.csdn.net/tao_627/article/details/79375950

唯一ID生成

  • 分布式系统唯一ID生成方案汇总 - https://www.cnblogs.com/wyb628/p/7189772.html

RabbitMQ

  • 我为什么要选择RabbitMQ ,RabbitMQ简介,各种MQ选型对比 - https://www.sojson.com/blog/48.html

反射 reflect

  • Go Reflect 性能 - https://colobu.com/2019/01/29/go-reflect-performance/

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


Builder & Option 设计模式

  • 实例浅谈利用Golang的Builder&Option设计模式来传递初始化参数 - https://www.toutiao.com/a6768276240735404558/?tt_from=mobile_qq&utm_campaign=client_share×tamp=1575900880&app=news_article&utm_source=mobile_qq&utm_medium=toutiao_ios&req_id=201912092214400100260790191A2D68A8&group_id=6768276240735404558

GC

  • 深入理解Go-垃圾回收机制 - https://juejin.im/post/5d78b3276fb9a06b1829e691

常见编译报错

  • 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

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

xf := xiaofang{}
_ = xf

踩坑

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

参考:

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

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

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

"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. 没有连上

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 操作, 都得加锁, 不然会有潜在的错误:

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

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

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 没被消费, 就是被过度消费. 例如:

// 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
*/

你可能感兴趣的:(Go)