代码格式化
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
fmt.Println(len([]rune(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会自动重启主进程,所以会出现主进程一直正常运行的情况。
对于错误和异常是如何处理的?
- 对于错误,我们自己写代码处理掉
- 对于异常(如协程上没有处理的 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 退出,这种情况下怎么办呢?