代码格式化

golang 自带的 go fmt 默认是是 tab 缩进, 而 goland IDE 的格式化默认是空格缩进【快捷键:option+command+L】

json的坑

[]uint8 转 json 后,得不到想要的结果

  • 期待结果:[1, 2, 3]
  • 输出结果:“AQID”
1
2
3
4
5
func main() {
   arr := []uint8{1, 2, 3}
   b, _ := json.Marshal(arr)
   fmt.Println(string(b))
}

slice/map/channel本身就是引用类型

slice

绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!

 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
// 一、同根
func main() {
    nums := [3]int{} // array
    nums[0] = 1

    fmt.Printf("nums: %v , len: %d, cap: %d\n", nums, len(nums), cap(nums))

    dnums := nums[0:2] // slice
    dnums[0] = 5

    fmt.Printf("nums: %v ,len: %d, cap: %d\n", nums, len(nums), cap(nums))
    fmt.Printf("dnums: %v, len: %d, cap: %d\n", dnums, len(dnums), cap(dnums))
}
# 输出结果
nums: [1 0 0] , len: 3, cap: 3
nums: [5 0 0] ,len: 3, cap: 3
dnums: [5 0], len: 2, cap: 3

// 二、时过境迁:随着 Slice 不断 append,内在的元素越来越多,终于触发了扩容。这时候内部就会重新申请一块内存空间,将原本的元素拷贝一份到新的内存空间上。此时其与原本的数组就没有任何关联关系了,再进行修改值也不会变动到原始数组。
func main() {
    nums := [3]int{}
    nums[0] = 1

    fmt.Printf("nums: %v , len: %d, cap: %d\n", nums, len(nums), cap(nums))

    dnums := nums[0:2]
    dnums = append(dnums, []int{2, 3}...)
    dnums[1] = 1

    fmt.Printf("nums: %v ,len: %d, cap: %d\n", nums, len(nums), cap(nums))
    fmt.Printf("dnums: %v, len: %d, cap: %d\n", dnums, len(dnums), cap(dnums))
}
# 输出结果
nums: [1 0 0] , len: 3, cap: 3
nums: [1 0 0] ,len: 3, cap: 3
dnums: [1 1 2 3], len: 4, cap: 6

// 三、参数默认是引用传值
func main() {
    x := []int{1, 2, 3}
    func(arr []int) {
        arr[0] = 7
        fmt.Println(x)    // [7 2 3]
    }(x)
    fmt.Println(x)    // [7 2 3]
}

map

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

import "fmt"

func main() {
    persons := make(map[string]int)
    persons["张三"] = 19

    mp := &persons

    fmt.Printf("原始map的内存地址是:%p\n", mp) // 输出:原始map的内存地址是:0xc00000e028
    modify(persons)
    fmt.Println("map值被修改了,新值为:", persons) // 输出:map值被修改了,新值为: map[张三:20]
}

func modify(p map[string]int) {
    fmt.Printf("函数里接收到map的内存地址是:%p\n", &p) // 输出:函数里接收到map的内存地址是:0xc00000e038
    p["张三"] = 20
}

单引号

单引号在 Golang 表示一个字符,使用一个特殊类型 rune 表示字符型。rune 为 int32 的别名,它完全等价于 int32,习惯上用它来区别字符值和整数值。rune 表示字符的 Unicode 码值。

1
2
3
4
func main() {
    var c rune = '你' // 此处必须使用单引号,而且只能写一个字符,不能这样定义:var c rune = 'ab'
    fmt.Printf("c=%v ct=%T\n", c, c) // 输出 c=20320 ct=int32 (字符’你’的 Unicode 码值是 0x4f60,十进制是 20320)
}

len & utf8.RuneCountInString

Go 的内建函数 len() 返回的是字符串的 byte 数量(或unicode编码数量);如果要得到字符串的字符数,可使用 “unicode/utf8” 包中的 RuneCountInString(str string) (n int)

func main() { x := “abc中国人” fmt.Println(len(x)) // 输出:12 fmt.Println(utf8.RuneCountInString(x)) // 输出:6 }

在一个值为 nil 的 channel 上发送和接收数据将永久阻塞

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    var ch chan int // 未初始化,值为 nil
    for i := 0; i < 3; i++ {
        go func(i int) {
            ch <- i
        }(i)
    }

    fmt.Println("Result: ", <-ch)
    time.Sleep(2 * time.Second)
}

golang 没有全局异常捕获

不能像其他语言那样,在最上层就可以捕获所有 exception

  • golang 所有的 goroutine 都有可能异常 panic,
  • 每个 goroutine 的 异常都需要分别捕获,
  • 每个 goroutine 的子 goroutine 的异常也都需要分别捕获

panic 和 recover 的组合有如下特性:

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

  • 有 panic 没 recover,程序宕机。
  • 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

使用 supervisor 部署脚本

协程内panic后,如果没有recover的话,主进程无法捕获协程的panic,主进程会直接挂掉。但我们是用supervisor运行主进程,supervisor会自动重启主进程,所以会出现主进程一直正常运行的情况。

对于错误和异常是如何处理的?

  1. 对于错误,我们自己写代码处理掉
  2. 对于异常(如协程上没有处理的 panic),系统会中断代码的运行,直接返回错误
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 解决方案,定义一个通用的方法
func GoSafe(f func(), v interface{}){
    go func(v interface{}){
        defer func(){
            if err := recover(); err != nil {
                log.Printf("panic: %+v", err)
            }
        }()

        f(v)
    }(v)
}

如何在http服务端程序中读取2次Request Body

在http服务端程序中,我想在真正处理Request Body之前将Body中的内容记录到日志中.

实际上这一需求就是要在Request Body中读取2次数据,由于Body为ReadCloser 类型,读取一次之后就无法再次进行读取,就需要读取完之后对Body重新赋值来支持后续的读取操作, 网上一般都是这样实现的.

1
2
bodyBytes, _ := ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

超时退出导致goroutine泄漏问题

做超时提前退出时,一定要注意代码中会不会出现 goroutine 泄漏!!!

在下方代码例子中,process 函数会启动一个 goroutine,去处理需要长时间处理的业务,处理完之后,会发送 true 到 chan 中,目的是通知其它等待的 goroutine,可以继续处理了。

我们来看一下下方代码第 10 行到第 15 行,主 goroutine 接收到任务处理完成的通知,或者超时后就返回了。这段代码有问题吗?

如果发生超时,process 函数就返回了,这就会导致 unbuffered 的 chan 从来就没有被读取。我们知道,unbuffered chan 必须等 reader 和 writer 都准备好了才能交流,否则就会阻塞。超时导致未读,结果就是子 goroutine 就阻塞在第 7 行永远结束不了,进而导致 goroutine 泄漏

解决这个 Bug 的办法很简单,就是将 unbuffered chan 改成容量为 1 的 chan(创建有缓冲区的channel),这样第 7 行就不会被阻塞了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func process(timeout time.Duration) bool {
    ch := make(chan bool) // 修改为:ch := make(chan bool, 1) 即可解决 goroutine 泄露问题

    go func() {
        // 模拟处理耗时的业务
        time.Sleep((timeout + time.Second))
        ch <- true // 第7行,block,此处初选 goroutine 泄露
        fmt.Println("exit goroutine")
    }()
    select {
    case result := <-ch:
        return result
    case <-time.After(timeout):
        return false
    }
}

设置缓冲区是一种方式,还有另一种方式:使用 select 尝试向信道 done 发送信号,如果发送失败,则说明缺少接收者(receiver),即超时了,那么直接退出即可。

select 优势:缓冲区不能够区分是否超时了,但是 select 可以(没有接收方,信道发送信号失败,则说明超时了)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func process(timeout time.Duration) bool {
    ch := make(chan bool) // 修改为:ch := make(chan bool, 1) 即可解决 goroutine 泄露问题

    go func() {
        // 模拟处理耗时的业务
        time.Sleep((timeout + time.Second))
        select {
            case ch <-true:
                fmt.Println("exit goroutine")
            default:
                return
        }
    }()
    select {
    case result := <-ch:
        return result
    case <-time.After(timeout):
        return false
    }
}

强制 kill goroutine 可能吗?

上面的例子,即时超时返回了,但是子协程仍在继续运行,直到自己退出。那么有可能在超时的时候,就强制关闭子协程吗?

答案是不能,goroutine 只能自己退出,而不能被其他 goroutine 强制关闭或杀死。

关于这个问题,Github 上也有讨论:

question: is it possible to a goroutine immediately stop another goroutine?

摘抄其中几个比较有意思的观点如下:

  • 杀死一个 goroutine 设计上会有很多挑战,当前所拥有的资源如何处理?堆栈如何处理?defer 语句需要执行么?
  • 如果允许 defer 语句执行,那么 defer 语句可能阻塞 goroutine 退出,这种情况下怎么办呢?