6

Go的一些坑

 3 years ago
source link: https://zsmhub.github.io/post/golang/go%E7%9A%84%E4%B8%80%E4%BA%9B%E5%9D%91/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

代码格式化

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

json的坑

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

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

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

slice

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

// 一、同根
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]
}
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 码值。

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 上发送和接收数据将永久阻塞

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),系统会中断代码的运行,直接返回错误
// 解决方案,定义一个通用的方法
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重新赋值来支持后续的读取操作, 网上一般都是这样实现的.

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 行就不会被阻塞了。

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 可以(没有接收方,信道发送信号失败,则说明超时了)。

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 退出,这种情况下怎么办呢?

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK