Go中神奇的内置数据结构

本节内容是自己学习 Go高级工程师实战营 课程中 神奇的内置数据结构一节所做的笔记

概览

概览图:y3h5kk

  1. semaphore是信号量,是所有锁实现的基础,iface与eface是interface底层实现相关的
  2. netpoll就是我们之前讲到的runtime的四座大山,里面也有一些自定义数据结构,会在之后讲网络编程或者web框架的时候说
  3. sync是同步相关的,sync.Mutex是互斥锁相关的,sync.Pool是对象池,sync.Once必须只在只能初始化一次的函数中使用,waitgroup是汇集多个并发执行的结果
  4. os比较简单,因为最复杂的在底层进行了封装,我们是看不到的
  5. 内存分配会专门在gc中讲解
  6. context是Go 1.6增加的数据结构,也是值得去讲的

channel

两种创建方式

  1. 不带缓冲的:其实是有buffer的特殊情况,可以理解为buffer size为0的buffered chan。ch := make(chan int)
  2. 带缓冲的:会生成一个hchan数据结构,dataqsize(队列的大小)将会是我们定义的缓冲区的大小,ch := make(chan int, 3)

channel结构

如下的hchan结构体就是我们channel的结构,点击查看channel源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters

// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}

一张图片生动形象的介绍channel的数据结构:9lOZyp

go

gin+docker部署Golang应用

使用gin编写Go的后端程序,然后使用docker打包成镜像并在我们的阿里云ECS上进行部署

编写Go后端程序

项目目录如下:

1
2
3
4
go-web
├── go.mod
├── main.go
├── Dockerfile

main.go文件内容如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"github.com/gin-gonic/gin"
"net/http"
"time"
)

/**
* @Author: yirufeng
* @Date: 2021/7/10 9:04 下午
* @Desc:
**/

type User struct {
Username string `json:"username"`
}

func main() {
engine := gin.Default()

engine.GET("/", func(c *gin.Context) {
startTime := time.Now()

c.JSON(http.StatusOK, gin.H{
"method": http.MethodGet,
"elapsedTime/ms": time.Since(startTime).Milliseconds(),
})
})

engine.POST("/", func(c *gin.Context) {
startTime := time.Now()

var u User
err := c.BindJSON(&u)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"method": http.MethodPost,
"elapsedTime/ms": time.Since(startTime).Milliseconds(),
"username": u.Username,
})
})

engine.Run(":8081")
}
go

go微服务环境搭建

本环境基于Macos搭建,运行的go版本是1.14.11,由于原先使用的1.15.5依赖包对应版本不存在或冲突修改后无法解决,所以只能修改go版本

沈逸老师go-micro搭建环境

搭建成功使用命令:

  1. go get github.com/micro/go-micro/v2
  2. go get github.com/micro/go-plugins/registry/consul/v2

参考:https://segmentfault.com/a/1190000023529475

搭建过程中遇到的问题:

1
2
3
4
5
6
7
8
9
# github.com/coreos/etcd/clientv3/balancer/resolver/endpoint
../../../../../go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/resolver/endpoint/endpoint.go:114:78: undefined: resolver.BuildOption
../../../../../go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/resolver/endpoint/endpoint.go:182:31: undefined: resolver.ResolveNowOption
# github.com/coreos/etcd/clientv3/balancer/picker
../../../../../go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/picker/err.go:37:44: undefined: balancer.PickOptions
../../../../../go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/picker/roundrobin_balanced.go:55:54: undefined: balancer.PickOptions
# github.com/micro/go-micro/transport/quic
../../../../../go/pkg/mod/github.com/micro/go-micro@v1.18.0/transport/quic/quic.go:54:12: q.s.Close undefined (type quic.Session has no field or method Close)
../../../../../go/pkg/mod/github.com/micro/go-micro@v1.18.0/transport/quic/quic.go:121:3: unknown field 'IdleTimeout' in struct literal of type quic.Config

解决办法:go.mod文件夹中加上下面两句话

1
2
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
replace github.com/lucas-clemente/quic-go => github.com/lucas-clemente/quic-go v0.14.1

参考:

  1. https://github.com/etcd-io/etcd/issues/12124#issuecomment-674368288
  2. https://github.com/etcd-io/etcd/issues/11931
  3. https://www.codenong.com/cs109250534/
  4. 推荐:https://studygolang.com/articles/32791

遇到的问题2:panic: qtls.ConnectionState not compatible with tls.ConnectionState

最后以上的问题都采用一种策略,删除原有go版本并且下载go1.14.11版本得到了解决,之后执行命令go get -u github.com/micro/go-micro
就是go的版本不要使用go1.15

consul集群搭建

参考博主文章

租房网搭建环境:

前言

如果网速很慢,经常io timeout,可以执行如下两条命令:

1
2
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

安装protoc

  1. 下载可执行文件
  2. 下载完成后,解压到~/Library,并修改文件夹名字为proto,并进入终端配置系统环境变量:export PATH=$PATH:$GOPATH/bin:$HOME/Library/protoc/bin
  3. 采用命令protoc --verison进行验证。配置成功,会有类似输出:libprotoc 3.11.2

安装protoc-gen-go

  1. go get -u github.com/golang/protobuf/protoc-gen-go
  2. 进入到目录:cd $GOPATH/src/github.com/golang/protobuf/protoc-gen-go
  3. 执行go build,生成可执行文件protoc-gen-go
  4. 将我们的可执行文件复制到$GOPATH/bin下:cp protoc-gen-go $GOPATH/bin

安装protoc-gen-micro

如果实在get不下来我们可以直接clone代码下来,然后go build

  1. go get github.com/micro/protoc-gen-micro
  2. 进入到$GOPATH/src/github.com/micro/protoc-gen-micro目录下,进行go build,生成可执行文件protoc-gen-micro
  3. 将我们的可执行文件复制到$GOPATH/bin下:cp protoc-gen-micro $GOPATH/bin

测试:在goland中新建一个项目,并且编写一个proto文件夹,编写demo.proto文件并填入如下内容

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
29
30
31
32
33
34
35
36
37
38
39
syntax = "proto3";

package myproto;

option go_package = "./";


//客户端发送给服务端的
message HelloRequest {
string name = 1;
}

//服务端返回给客户端的
message HelloResponse {
string msg = 1;
}


//客户端向服务端发送的请求
message NameRequest {
string name = 1;
}

//服务端返回给客户端的响应
message NameResponse {
string msg = 1;
}



//定义服务
service HelloService {
//函数的传入和传出就是我们自己要定义的消息类型
//一个打招呼的函数
rpc SayHello (HelloRequest) returns (HelloResponse) {}
//一个说名字的函数
rpc SayName (NameRequest) returns (NameResponse) {}
}

进入到proto文件所在的目录,执行protoc --proto_path=$GOPATH/src:. --micro_out=. --go_out=. *.proto
当前目录下生成两个文件:demo.pb.go和demo.pb.micro.go

安装go-micro

执行命令:go get github.com/micro/go-micro/v2@latest

安装micro

方式一:

执行命令:go get github.com/micro/micro/v2@latest

通过该方法安装的micro默认使用的服务发现组件是mdns,没有集成Consul、Etcd等第三方的组件。我们后面需要使用Consul,所以此时需要自己手动编译安装。

方式二:

  1. git clone https://github.com/micro/micro.git
  2. 进入到clone后的文件夹,新建plugins.go文件,键入如下内容:
    1
    2
    3
    4
    5
    package main

    import (
    _ "github.com/micro/go-plugins/registry/consul/v2"
    )
  3. 最后,重新编译安装:go install
  4. 完成后,执行如下命令检查micro是否安装成功:micro --version。如果有类似输出说明安装成功:
    1
    micro version 2.0.0

方法三:直接下载二进制文件

  1. 点击下载地址进行下载
  2. 将我们的micro可执行程序移动到/usr/local/bin/

Golang中使用gdb调试工具

gdb 工具的安装

!> 以mac系统为例

1
brew install gdb

使用场景

通常情况下,我们使用gdb是为了研究底层源码的实现,比如go中的runtime的入口在哪里?

go中使用

  1. 编译go程序的时候,要带上编译信息,也就是使用命令go build --gcflags=all="-N -l" .
  2. 执行命令 gdb 二进制文件的名字 就可以进入gdb调试了

gdb 中常用命令的使用

note info 注意:gdb中命令行有一个特点,如果我们在命令中没有任何输入按下回车将会重复上一个命令,但是有一些命令不会,例如run

  • list:将当前行号所在的代码打印出来

  • 加一个断点:break 断点所在的行 或者 b 断点所在的行

  • 当我们执行run命令之后将会停在第一个断点上,然后使用nextns执行到下一行,如果想要跟踪进去调用函数的内部,我们使用命令s进入调用函数的内部,如果我们想要退出该函数了,我们可以使用命令return退出返回到调用代码的地方,如果想要查看某一个变量的值,可以使用命令p 变量名,如果我们不想每次都打印指定的变量,我们可以使用info args打印参数或info locals打印本地变量
    next:执行下一行
    b613L7
    OnudWS

  • 使用info files命令,看到有一个entry point,也就是代码是从这里开始执行的,我们想要让代码在入口停下,我们在入口加上一个端点,也就是在entry point对应的值的地址加上断点b 对应的地址
    rAQI5Q

  • list。列出代码,输入第一次后如果输入回车,那么就会重复以上命令

  • break。加断点,一般是list之后,break 行号来加断点

  • bt。打印调用链

  • info files。打印调试文件信息

  • run。运行所要调试的代码

  • up 和 down。在frame里跳来跳去

  • info args 和 info locals 打印参数和本地变量

  • whatis 和 p。打印变量和想要看的值,例如数组啊,函数啊,都可以

  • info goroutines。查看所有的goroutine及其ID

  • goroutine 命令。对对应的goroutine执行命令。

  • q。退出

  • help。打印帮助文档

参考

工具

jwt-go的使用

了解JWT

jwt简称为json web token,一般用于用户认证(前后端分离/微信小程序/app开发)。紧凑且安全,特别适合分布式站点的单点登录

JWT通常用在请求头的Authorization字段中,形如:Authorization: Bearer <token>,也是最标准传递JWT的方法

jwtgo

golang中的defer-recover-panic

panic

panic其实就是我们其他语言中的throw exception

recover

recover其实就是其他语言的try catch

go

golang中的iota

经常有面试官甩出个const以及iota的面试题

如下便是国内某公司的面试题:

回答输出结果

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
29
30
31
32
package main

import "fmt"

/**
* @Author: yirufeng
* @Email: yirufeng@foxmail.com
* @Date: 2020/10/13 6:06 下午
* @Desc:
*/


//第一个const来自于面试题
const (
i = 7
j
k
)

const (
a = 7
b
c = 3
m = 1 << iota
n
o
)

func main() {
fmt.Println(a, b, c)
fmt.Println(i, j, k, m, n, o)
}

总结

进行几点总结

  1. 不同 const 定义块互不干扰:即我们在推算这些常量值的时候只需要看当前的 const ( ) 之内的内容,不用关心之前或之后是否也用常量定义语句
  2. 所有注释行和空行全部忽略:
    1. 注意:_ 并不是一个空行,它是一个省略了标识符也省略了表达式的常量定义,这一点你需要清楚,不要大意。
  3. 没有表达式的常量定义复用上一行的表达式:例如如下的代码,j中没有写表达式,那么j是复用上一行表达式的即j = iota,但此时iota的值会为2
    1
    2
    3
    4
    const (
    i = iota
    j
    )
  4. 从第一行开始,iota 从 0 逐行加一:也就是说赋值表达式里无论是否引用了 iota,也无论引用了多少次,iota 的都会从常量定义块的第一行(注意这里不计空行和注释)开始计数,从 0 开始,逐行加一,例如上面代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const (
    i = 7 //iota = 1
    j //iota = 2
    k //iota = 3
    )

    const (
    a = 7 //iota = 1
    b //iota = 2
    c = 3 //iota = 3
    m = 1 << iota //iota = 4
    n //iota = 5
    o //iota = 6
    )
  5. 替换所有 iota:直接将表达式中的iota全部替换为我们上一步写出的值即可
go

golang中命令行参数的使用

使用os.Args获取输入的命令行参数

os.Args获取输入的命令行参数

  1. os.Args本质上是一个切片,会将命令行的内容全部读取,只是将我们输入的内容按照空格进行分割,其中第1个是执行的程序的全路径名称,
  2. 后面的为我们执行的时候命令行上的其他参数

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"os"
)

/**
* @Author: yirufeng
* @Email: yirufeng@foxmail.com
* @Date: 2020/10/6 6:00 下午
* @Desc: os.Args练习


本质上是一个切片,会将命令行的内容全部读取,只是将我们输入的内容按照空格进行分割,其中第1个是程序执行的名称,
后面的为我们执行的时候命令行上的其他参数
*/
func main() {
fmt.Println(len(os.Args))
fmt.Println(os.Args)
}

使用flag包来解析命令行参数

具体步骤

  1. StringVar,IntVar,BoolVar需要传入4个参数
    1. 第1个参数为接收值的变量的地址
    2. 第2个参数为我们使用命令行中的-后面紧跟的字符作为key
    3. 第3个参数为默认值
    4. 第4个参数为参数说明(命令设置错误时的说明)
  2. 使用flag包解析命令行参数最后一定要记得flag.Parse()之后,我们上面的设置的解析参数才会生效

到时候执行如下代码的时候,我们需要先使用go build指令进行编译,编译之后运行可执行文件的时候后面还得加上参数,这里我们应该加上./demo -h 10.26.166.14 -p 3309 -o false 如果不指定参数的时候我们将会使用程序中设置的默认值来读取

go

golang中关于文件的操作

终端读写操作note info 默认主题色,适合中性的信息 终端读写相关的文件句柄常量os.Stdin 标准输入os.Stdout 标准输出os.Stderr 标准错误以上3个都是以文件的形式存在
go

golang中的错误处理

自定义错误

  1. 因为error是一个接口,因此我们可以自定义一个error
  2. 实际开发中我们要判断错误的类型,直接进行类型断言(也可以通过switch进行类型断言分支)

三部曲自定义一个错误

  1. 自定义一个错误结构体
  2. 实现我们的Error方法
  3. 返回错误的时候直接返回一个&我们自己的错误

注意:我们最常用的就是errors.New(字符串)