golang tips in ali

高频技艺

函数集合

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
// Op 保存对 msg 处理的函数集合
type Op func(msg any) (any, error)

// 解码远程消息
func decode(msg any) Op {
return func(any) (any, error) {
fmt.Println("decoding ...", msg)
decodedRes := fmt.Sprintf("decoded_%v", msg)
fmt.Println("decoded to parameter->", decodedRes)
return decodedRes, nil
}
}

// 模拟业务处理
func opAction(parameter any) Op {
return func(any) (any, error) {
fmt.Println("do opAction ...", parameter)
opRes := fmt.Sprintf("opAction_%v", parameter)
fmt.Println("after opAction result->", opRes)
return opRes, nil
}
}

// 模拟结果处理
func encode(result any) Op {
return func(any) (any, error) {
fmt.Println("encoding ...", result)
encodeRes := fmt.Sprintf("encoded_%v", result)
fmt.Println("after encoded result->", encodeRes)
return encodeRes, nil
}
}

这里的 Op 定义了对 msg 处理的『函数集合』,function 在 Golang 当中是一等公民,类 Java Lambda 表达式,使用 Golang Function 可以方便定义函数算子,真正实现函数式编程。

实际使用中,可以定义多种 Op,放到 map 中去,可以根据一些信息比如 message tag 做一些路由处理

Context 教学

go 中 context 是用来管理程序中跨越多个 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
29
30
31
32
33
34
35
36
37
func NewCtx() {
bg := context.Background() // 顶层 ctx
fmt.Println(bg)
todo := context.TODO() // 不清楚使用哪种 ctx 或者当前函数之后会更新 ctx
fmt.Println(todo)
}

// 1.ctx 不可变
// 2.withValue 会基于 pCtx 生成 cCtx
func CtxWithValue() {
ctxA := context.Background()
ctxB := context.WithValue(ctxA, "b", "B")
ctxC := context.WithValue(ctxA, "c", "C")
ctxD := context.WithValue(ctxB, "d", "D")
ctxE := context.WithValue(ctxB, "e", "E")
ctxF := context.WithValue(ctxC, "f", "F")
fmt.Println(ctxA) // a
fmt.Println(ctxB) // a->b
fmt.Println(ctxC) // a->c
fmt.Println(ctxD) // a->b->d
fmt.Println(ctxE) // a->b->e
fmt.Println(ctxF) // a->c->f
}

// 1. cCtx 可以访问 pCtx
// 2. 分支路径上的 ctx 之间不能互相访问
// 3. pCtx 不能访问 cCtx 中的值
func AccessParentContextValue() {
ctxA := context.Background()
ctxB := context.WithValue(ctxA, "b", "B")
ctxC := context.WithValue(ctxA, "c", "C")
ctxF := context.WithValue(ctxC, "f", "F")
fmt.Println(ctxF.Value("f")) // 访问 self
fmt.Println(ctxF.Value("c")) // 访问 pCtx
fmt.Println(ctxF.Value("b")) // 分支路径上的 ctx 不能访问
fmt.Println(ctxB.Value("f")) // pCtx 不能访问 cCtx
}

普通异常处理

使用原生 error package + 直接比较。

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
var (
errorNotFound = errors.New("item not found")
errorMissingParam = errors.New("missing param")
errorUnknown = "unKnownErr"
)

func HandlingErrorInSimpleCase(key string) {
val, err := getItem(key)
if err != nil {
switchErrByComparison(err)
return
}
fmt.Println(val)
return
}

func getItem(key string) (any, error) {
cn := make(map[string]string)
val, ok := cn[key]
if !ok {
return nil, errorNotFound
}
return val, nil
}

func switchErrByComparison(err error) {
if err == nil {
return
}
switch err {
case errorNotFound:
fmt.Println(errorNotFound.Error())
case errorMissingParam:
fmt.Println(errorMissingParam.Error())
default:
fmt.Println(errorUnknown)
}
}

如果有根据 errType 分组的需求应该如何实现呢?

自定义业务异常

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
type errorType string

const (
errorNotFound errorType = "item not found"
errorMissingParam errorType = "missing param"
errorUnknown errorType = "unKnownErr"
)

type BusinessError struct {
errorType errorType
msg string
}

func NewBusinessError(errorType errorType, msg string) *BusinessError {
return &BusinessError{errorType, msg}
}

func (e *BusinessError) Error() string {
return fmt.Sprintf("%v_%v", e.errorType, e.msg)
}

func switchErrByType(err error) {
if err != nil {
switch err.(type) {
case *BusinessError:
fmt.Println(err.Error())
default:
fmt.Println(errorUnknown)
}
}
}

Panic 处理

panic 主要处理程序出现的异常,而非业务异常:

  1. panic(errMsg) 显式抛出异常
  2. recover 可以捕捉 panic,但是只能在 defer 中使用,可以将异常 goroutine 从 panic 中恢复

    1
    2
    3
    4
    5
    defer func() {
    if err := recover(); err != nil {
    fmt.Println(err)
    }
    }()
  3. recover 只在当前 goroutine 有效,不能跨 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
func PanicSimulate(key int) {
defer func() {
// 只对当前 goroutine 有效
if err := recover(); err != nil {
fmt.Println(err)
}
}()

if key == 2 {
panic(fmt.Sprintf("key == 2"))
}

fmt.Println("run ok")
}

func runRoutines() {
for i := 0; i < 3; i++ {
go PanicSimulate(i)
}
// 保持 goroutines 运行
for {
select {
default:
}
}
}

sync.atomic 共享变量

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
func UnSafeAdd() {
var n int = 0
var wg *sync.WaitGroup = new(sync.WaitGroup)
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func(w *sync.WaitGroup, num *int) {
defer w.Done()
*num = *num + 1 // 这个操作不是原子的
}(wg, &n) // 最终结果 n 不等于 1000
}
wg.Wait()
}

func SafeAdd() {
var n int32 = 0
var wg *sync.WaitGroup = new(sync.WaitGroup)
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func(w *sync.WaitGroup, num *int32) {
defer w.Done()
atomic.AddInt32(num, 1) // 操作是原子的
}(wg, &n) // 最终 n 等于 1000
}
wg.Wait()
}

sync.Mutex 上锁

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
func SafeAddAndMinus() {
var num int = 0
var mutex = new(sync.Mutex)
var wg = new(sync.WaitGroup)
wg.Add(2)
// add
go func(*sync.Mutex, *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1000; i++ {
mutex.Lock()
num++
mutex.Unlock()
}
}(mutex, wg) // 加 1000 次
// minus
go func(*sync.Mutex, *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1000; i++ {
mutex.Lock()
num--
mutex.Unlock()
}
}(mutex, wg) // 减 1000 次
wg.Wait()
}

sync.Cond 消费者范式

在生产者消费者模型中,通常会使用锁(如互斥锁)来保证数据的一致性:

  1. 生产者/消费者先获取锁
  2. 如果获取失败,则进入睡眠
  3. 生产者/消费者处理完成后,会通知消费者/生产者继续执行

即:加锁+循环&等待+唤醒

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
const MIN_ICE_CNT = 0
const MAX_ICE_CNT = 3
type iceCube int
type cup struct {
iceCubes []iceCube
}

func simulateProducerAndConsumer() {
stopCh := make(chan struct{})
lc := new(sync.Mutex)
cond := sync.NewCond(lc)
cup := cup{
iceCubes: make([]iceCube, MIN_ICE_CNT, MAX_ICE_CNT),
}
// consumer
go func() {
for {
cond.L.Lock()
for len(cup.iceCubes) == MIN_ICE_CNT {
cond.Wait()
}
cup.iceCubes = cup.iceCubes[1:]
cond.Signal()
cond.L.Unlock()
}
}()
// producer
go func() {
for {
cond.L.Lock()
for len(cup.iceCubes) == MAX_ICE_CNT {
cond.Wait()
}
cup.iceCubes = append(cup.iceCubes, 1)
cond.Signal()
cond.L.Unlock()
}
}()

for {
select {
case <-stopCh: // 等待通道关闭,或者一直执行下去
return
default:
}
}
}

json 结构定义

json2struct idea 自带了,不赘述。

项目生成类图

goPlantUml 插件,用 plantUml 生成 uml,可以代码自动生成。

标准目录

  • api:对外暴露 api
  • cmd:项目入口
  • conf:项目配置文件
  • constant:常量
  • docs:设计文件
  • internal:组件,与业务无关,比如 errors 等
  • pkg:更纯净的组件,每个包都可以独立出来作为一个库
  • version:当前版本
  • go.mod:第三方依赖

性能优化

go tool pprof 是专门用来定位 go 项目性能瓶颈工具,不赘述。

最佳实践

好的开始是成功的一半

  • 开发环境这里面我推荐大家要不就用 idea 原生的,要不就用 Goland 的,这两个其实是比较适合去开发 Go 的,不太推荐大家用什么 VScode,或者 VIM 去做这个事情。如果遇到像 Go mod 的时候,或者做一些三方应用的时候,其实 VScode 会有一些 bug,就是调试起来会比较麻烦。
  • 代码格式化这里面强烈推荐大家用 gofmt 去统一做,它能帮你将这个代码统一的去做一个格式化。lint 这一块还是选择性开启吧。因为 lint 这个东西它在编译的时候,包括你在开发的时候,它会影响你的开发流畅度。
  • 不要用 common 库的方式。所谓common库,就是一个全家桶,就是我所有所谓的这种工具类也好啊,util 包也好,我就弄成一个;包的职责 Go 这边推荐要单一。
  • 包不要有状态,所谓的这个包的状态就是说有一些内部的状态机全局变量,这种都是要尽量避免的。
  • 一般 Main 函数做什么呢?做这个 flag 参数解析,另外就是初始化一些配置,或者说把 log 提前都打好,然后再把数据库或者网络连接池,把它做好就可以了。
  • 尽量 Go 不要去做这个 init 里面的开发,init 一般在什么时候用呢?它会在这个比如说你要注册一个 GUB 的时候,注册一个序列化的对象的时候,可以在 init 里面提前去注册。其他的场景其实暂时也不多。

基本规范

  • 开发的规范就是驼峰式,基本上和 java 什么的都是类似的。
  • Go 有 4 个引用的,就是默认的传参,它已经是引用了,所以说我们不用传指针,就是 map、chan,还有这个函数 interface 这4个。
  • 永远不要去启动一个停止不了的协程,不要让它永远阻塞在那。
  • 你要开一个协程,要把这个开协程的授权调用授权给他的这个调用方。
  • 上图左边的和右边的都有一些问题,比较好的写法是有一个退出信号,如果有调用方希望去 Go,那他就 Go 出来。

错误处理

  • 好比 json 的这个序列化的时候,其实在基本上 99.99% 的情况他都不会出错。他一旦出错了,那一定是不可预期的。

让别人参与进来

  • 如果大家都不习惯用 Go 的这种默认测试框架,可以试试 testify。
  • 日志和这个代码我认为都是给人看的,不是给机器看的,所以说日志一定要上下文能够清晰的表达出来你的这个逻辑或者你的这个含义。
  • 不要注释那种不好的代码,直接重构它。
  • 要敢于say no,就是有一些不合理的设计也好,或者不合理的需求也好,我们要有这种意识去说。
  • 要掌握这种分布式的编程的思维,这种稳定性的这种的能力需要大家建立起来这种的意识。如果他出现了这种核心的 panic 也好,或者说宕机了,或者数据错了,我们要在心中就把握好那个度,要兜这个底。要知道他一个最坏的一个情况。

养好服务

  • pprof 这个 server,不要暴露在公网,如果暴露在公网了。
  • 二进制的安全更多是放在一些 IDC 机房,因为这些资源它是不可控的。一旦你的磁盘坏了或者淘汰了你的这个二进制的应用很可能会被泄露。(可以使用一些混淆)
  • Go 会有一个开源的三方的一个 ANT 的库。这块其实它是一个协程池,节省资源。
  • 漏斗原则,我在做一个什么 HTTPserver 也好啊,或者做一个什么消息队列服务也好啊,要有这种流控或者这种开关,要有这种 Quota,说白了不要去一股脑的把资源放给客户或者三方应用去使用。
  • 这个操作系统层面需要知道这个 system 这个 rlimit 这个句柄。这个句柄现在是默认是 65535,或者没开的话,也就 1024。这块的话需要去注意一下,在跑这个应用性能的时候,这个句柄限制需要大家选择性的去开或者关。
    • 也可以用一些大招,比如说 cgroup 或者 Docker 这种去做一个物理上的一个资源层面上的隔离。
  • 告警一定要能处理,否则这个告警其实是没有任何意义的。

注意事项

  • 新手容易犯的错误就是这个闭包引入外部变量。
    • 这个外部变量是个引用,你在循环调用它的时候,它每次这个值都是一个指针。所以说你在传值的时候一定要传他具体的这个对象,而不要传他的指针。
  • defer 他跟这个函数的作用域无关,他只会在这个函数退出的时候执行。
    • 第一个的结果其实会先打出来 1 3 5 7 9,然后再把 10 8 6 4 2 打出来。
    • 第二个其实是正常的,他的逻辑就是说我的这个 defer 只会在这个函数退出的时候执行。

核心类型

  • map 是一个无序的集合。这块要注意一下,如果想用有序的,谷歌有一个开源的 BTree 的一个 map 是可以去用的,也是一个开源的三方包。
  • channel 是一个有锁的环形队列。无论它读、写也好啊,读码、写码也好啊,它都会 channel。无论你有没有缓冲区,就是你写满了读没了 channel 其实是都会阻塞的。
  • slice 不支持并发,动态的扩缩容,实际工程中大家会比较少用。

  • interface 这个数据类型它不是任意的类型,它不是 C++ 这个 void*,它其实还是一个对象,做类型转换的时候,比较这个 interface 它一定不是空。因为它在做这个类型转换的时候,它会隐式的将这个 interface 转换成这个对象的这个具体类型。它不仅仅包含了这个转换前变量的这个信息,还包含了这个类型的信息。

八股文

  • 你的本地是什么 1.17 的,然后线上的是 1.13,那就可以编译不过,而且你排查这个问题还会很麻烦,很费劲。
  • 外如果是大家用 docker 比较多的话,要注意一下这个 runtime.GOMAXPROCS 这块他其实是获取的是这个宿主机的 CPU 的核,不是真正的 docker 的那个 CPU 的核。

书籍推荐

  • Go 圣经
  • Go 语言高级编程
  • Go 语言原本
  • DDIA

总结