序言
分享目标:针对go中经常出现的坑有一个意识。防止在平常coding中出现类似的错误,或者调试有bug时能迅速回忆起讲过的地方。不对原理进行过多的深入。
为什么序号不按顺序: 其中小标题的序号代表原著的序号,方便大家快速找到对应原著中的位置,原著在底部参考书籍处。
Go 100mistakes选择其中容易犯和常见的错误一起讨论。
3 错误的初始化
不要依赖Go的init()顺序,Go是根据引入包名字母决定的初始化顺序,如果修改了包名则可能引发意想不到的错误。
一个包中可以定义多个init函数,在这种情况下,包内的init函数的执行顺序是基于源文件的字母顺序。例如,如果一个包包含一个a.go和b.go文件,并且文件中都有init函数,则首先执行的是a.go中的init函数。但是,我们不应该依赖包中init函数的执行顺序,因为这种做法很危险,像重命名源文件会导致顺序重排,从而可能影响执行顺序。
20 未理解slice长度和容量
- 截断操作or直接赋值后底层会共用一个数组,直到其中一个发生扩容。
- append可能会产生意料之外的结果。
func main() {
s1 := make([]int, 3, 6)
s2 := s1[1:3]
s1[1] = 1
print(s2)
s2 = append(s2, 2)
print(s2)
s1 = append(s1, 3) // append的3会覆盖第7行
print(s1)
print(s2)
}
func print(s []int) {
fmt.Printf("len=%d, cap=%d: %v\n", len(s), cap(s), s)
}
## print
len=2, cap=5: [1 0]
len=3, cap=5: [1 0 2]
len=4, cap=6: [0 1 0 3]
len=3, cap=5: [1 0 3]
| 1. | 2. |
|---|---|
| 3. | 4. |
- 实际业务中传递切片的时候,如果有截断等操作要注意 append 产生的意料之外结果。
func main() {
s := []int{1, 2, 3}
f(s[:2])
fmt.Println(s) // [1 2 10]
}
func f(s []int) {
_ = append(s, 10)
}
可采取方案
- 深拷贝
func f(s1 []int) {
_ = append(s1, 10)
}
func listing2() {
s := []int{1, 2, 3}
sCopy := make([]int, 2, 3)
copy(sCopy, s)
f(sCopy)
result := append(sCopy, s[2])
fmt.Println(result)
}
- 使用s[low:high:max]表达式,限制cap强制append时进行扩容
s := []int{1, 2, 3}
f(s[:2:2])
fmt.Println(s)
22 slice的nil和empty
// nil slice
var s []string
// empty slice
s = make([]string, 0)
- 不管是nil还是还是empty slices 调用
len(slice)都等于0 - nil切片不会分配内存,空切片会分配内存
- 如果能够确定最后返回的切片为空,则推荐使用 var s []string, 如果在初始化时已知道切片的长度,则采用make([]string,length)最好
24 正确的对slice使用copy函数
- copy函数将源切片中的数据拷贝到目标切片时,拷贝的元素个数为下面两个长度中较小的一个。
func bad() {
src := []int{0, 1, 2}
var dst []int
copy(dst, src)
fmt.Println(dst)
_ = src
_ = dst
}
---- 输出
[]
---
func correct() {
src := []int{0, 1, 2}
dst := make([]int, len (src) )
copy(dst, src)
fmt.Println(dst)
_ = src
_ = dst
}
---- 输出
[0 1 2]
---
26 slices的内存泄漏
func getMessageType(msg []byte) []byte { return msg[:5] } func getMessageTypeWithCopy(msg []byte) []byte { msgType := make([]byte, 5) copy(msgType, msg) return msgType } func receiveMessage() []byte { return make([]byte, 1_000_000_000) } func printAlloc() { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%d KB\n", m.Alloc/1024) } func main() { printAlloc() msg := receiveMessage() five := getMessageTypeWithCopy(msg) printAlloc() runtime.GC() runtime.KeepAlive(five) // 保持对five的引用 printAlloc() }
使用 five := getMessageTypeWithCopy(msg) --- gc可回收
--- 输出
23567 KB
1000136 KB
23570 KB
----
替换成 five := getMessageType(msg) --- gc无法回收
123 KB
976697 KB
976698 KB
初始化的msg数组长度为1_000_000_000,取前5个元素返回。如果仍然保持msg[:5]的引用,会导致后面的所有元素都无法回收。
可采取解决方案:深拷贝
28 Maps的内存泄漏
func main() {
// Init
n := 1_000_000
m := make(map[int][128]byte)
printAlloc()
// Add elements
for i := 0; i < n; i++ {
m[i] = randBytes()
}
printAlloc()
// Remove elements
for i := 0; i < n; i++ {
delete(m, i)
}
// End
runtime.GC()
printAlloc()
runtime.KeepAlive(m)
}
func randBytes() [128]byte {
return [128]byte{}
}
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d MB\n", m.Alloc/1024/1024)
}
$ go run main2.go
0 MB
461 MB
293 MB
当删除了所有 kv 后,内存占用依然有 293 MB,这实际上是创建长度为 100w 的 map 所消耗的内存大小。map的buckets创建后就不会缩容。map中的buckets只会增加不会减少。
- 当 val 大小 <= 128B 时,val 其实是直接放在 bucket 里的。 会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。
- 当 val 大小 >= 128B 时,val会转为指针存储。
解决:
- 定期拷贝map到另一个map;
- 将 val 类型改成指针,可以减少buckets占用的内存;
30 忽略了元素在range循环中被复制的事实
-
range循环会拷贝
- 如果是结构体则会拷贝一份
- 如果是指针则拷贝指针的地址
package main
import (
"fmt"
"strings"
)
type account struct {
balance float32
}
func main() {
accounts := createAccounts()
for _, a := range accounts {
a.balance += 1000
}
fmt.Println(accounts) // [{100} {200} {300}]
accounts = createAccounts()
for i := range accounts {
accounts[i].balance += 1000
}
fmt.Println(accounts) // [{1100} {1200} {1300}]
accounts = createAccounts()
for i := 0; i < len(accounts); i++ {
accounts[i].balance += 1000
}
fmt.Println(accounts) // [{1100} {1200} {1300}]
accountsPtr := createAccountsPtr()
for _, a := range accountsPtr {
a.balance += 1000
}
printAccountsPtr(accountsPtr) // [{1100} {1200} {1300}]
}
func createAccounts() []account {
return []account{
{balance: 100.},
{balance: 200.},
{balance: 300.},
}
}
func createAccountsPtr() []*account {
return []*account{
{balance: 100.},
{balance: 200.},
{balance: 300.},
}
}
func printAccountsPtr(accounts []*account) {
sb := strings.Builder{}
sb.WriteString("[")
s := make([]string, len(accounts))
for i, account := range accounts {
s[i] = fmt.Sprintf("{%.0f}", account.balance)
}
sb.WriteString(strings.Join(s, " "))
sb.WriteString("]")
fmt.Println(sb.String())
}
[{100} {200} {300}]
[{1100} {1200} {1300}]
[{1100} {1200} {1300}]
[{1100} {1200} {1300}]
31 忽略了range循环参数怎样工作的
Range slice
- range循环在一开始就会拷贝一份range后面的参数
func main() {
s1 := []int{0, 1, 2}
for range s1 { // 不会死循环
s1 = append(s1, 10)
}
s2 := []int{0, 1, 2}
for i := 0; i < len(s2); i++ { // 死循环
s2 = append(s2, 10)
}
}
Range channel
- 由于range循环时ch已经是一份copy值了,尽管ch=ch2(25行)range循环依然迭代的ch1
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
go func() {
ch1 <- 0
ch1 <- 1
ch1 <- 2
close(ch1)
}()
ch2 := make(chan int, 3)
go func() {
ch2 <- 10
ch2 <- 11
ch2 <- 12
close(ch2)
}()
ch := ch1
for v := range ch {
fmt.Println(v)
ch = ch2
}
}
--- 输出
0
1
2
Range array
package main
import "fmt"
func listing1() {
a := [3]int{0, 1, 2}
for i, v := range a {
a[2] = 10
if i == 2 {
fmt.Println(v) // 2
}
}
}
func listing2() {
a := [3]int{0, 1, 2}
for i := range a {
a[2] = 10
if i == 2 {
fmt.Println(a[2]) // 10
}
}
}
func listing3() {
a := [3]int{0, 1, 2}
for i, v := range &a {
a[2] = 10
if i == 2 {
fmt.Println(v) // 10
}
}
}
- range循环仅在循环开始之前通过复制(无论类型如何)对提供的表达式求值一次。 我们应该记住这种行为,以避免可能导致我们访问错误元素等常见错误。
32 忽略range循环指针的影响范围
package main
import "fmt"
type Customer struct {
ID string
Balance float64
}
type Store struct {
m map[string]*Customer
}
func main() {
s := Store{
m: make(map[string]*Customer),
}
s.storeCustomers([]Customer{
{ID: "1", Balance: 10},
{ID: "2", Balance: -10},
{ID: "3", Balance: 0},
})
print(s.m)
}
// 是指针,所以map三次存储的都是同一个地址
func (s *Store) storeCustomers(customers []Customer) {
for _, customer := range customers { // customer会进行拷贝导致存储都是同一个地址
fmt.Printf("%p\n", &customer)
s.m[customer.ID] = &customer
}
}
func print(m map[string]*Customer) {
for k, v := range m {
fmt.Printf("key=%s, value=%#v\n", k, v)
}
}
---- 打印
0x14000098018
0x14000098018
0x14000098018
key=2, value=&main.Customer{ID:"3", Balance:0}
key=3, value=&main.Customer{ID:"3", Balance:0}
key=1, value=&main.Customer{ID:"3", Balance:0}
33 map迭代过程中的错误假设
错误:
- map循环是有序的
- map迭代中插入元素
func listing1() {
m := map[int]bool{
0: true,
1: false,
2: true,
}
for k, v := range m {
if v {
m[10+k] = true
}
}
fmt.Println(m)
}
--- 多次运行答案不一样
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true]
map[0:true 1:false 2:true 10:true 12:true 20:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true 40:true]
不要在map迭代过程中进行插入
39 性能不高的字符串连接
- string是不可变的,每次执行s += "xx"操作时会重新分配一片内存,字符串连接性能比较低
- strings.Builder{}比较快,对于已经知道要拼接的长度时可使用Grow()预分配(与slice类似)
- 在简单的字符串拼接上也可以使用s += "xx"的形式。毕竟strings.Builder的代码可读性不如s += "xx"
// 普通
func concat1(values []string) string {
s := ""
for _, value := range values {
s += value
}
return s
}
// strings.Builder 但是没有预分配
func concat2(values []string) string {
sb := strings.Builder{}
for _, value := range values {
_, _ = sb.WriteString(value)
}
return sb.String()
}
// strings.Builder 有预分配
func concat3(values []string) string {
total := 0
for i := 0; i < len(values); i++ {
total += len(values[i])
}
sb := strings.Builder{}
sb.Grow(total)
for _, value := range values {
_, _ = sb.WriteString(value)
}
return sb.String()
}
--- 数组长度1000,每个string长度1000下的对比
BenchmarkConcatV1-4 16 72291485 ns/op
BenchmarkConcatV2-4 1188 878962 ns/op
BenchmarkConcatV3-4 5922 190340 ns/op
42 使用什么类型作为方法的接收者
package main
import "fmt"
type customer struct {
balance float64
}
func (c customer) add(v float64) {
c.balance += v
}
type customerPoint struct {
balance float64
}
func (c *customerPoint) add(operation float64) {
c.balance += operation
}
func main() {
c := customer{balance: 100.}
c.add(50.)
fmt.Printf("balance: %.2f\n", c.balance) // 100
cp := customerPoint{balance: 100.0}
cp.add(50.0)
fmt.Printf("balance: %.2f\n", cp.balance) // 150
}
必须为指针
- 当方法需要改变接受者时。如果接收者是slice,并且需要向slice中添加元素,必须选择指针作为接收者。
- 当接收者包含不能拷贝的字段时,例如,对于sync包中的字段,像
sync.Mutex,只能选择指针作为接收者。
建议为指针
- 大对象时,使用指针相比值有更高的效率。通过benchmark测试可以评估
必须是值
- 不希望函数修改接收者情况
建议是值
- 当接收者是小的数组或者struct对象。并且不包含可以修改的字段。
43 从不使用命名返回值参数
// 命名返回
func f(a int) (b int) {
b = a
return
}
// 非命名返回
func f(a int) int {
b := a
return b
}
-
使用命名返回值(主要提供代码可读性时)
- 在大多数情况下,在接口中定义的上下文中使用有名参数可以提高代码可读性,而不会产生任何副作用
-
// 不知道具体的经纬度对应哪个参数 type locator interface { getCoordinates(address string) (float32, float32, error) } type locator interface { getCoordinates(address string) (lat, lng float32, err error) } - 在方法实现的上下文中,没有严格的规则,例如如果两个参数类型相同的时候,使用有名参数可以提高代码可读性。也可以利用初始化减少代码。
-
func ReadFull(r io.Reader, buf []byte) (n int, err error) { for len(buf) > 0 && err == nil { var nr int nr, err = r.Read(buf) n += nr buf = buf[nr:] } return }
45 返回零接收器(nil != nil)
type TradeError struct {
ErrNo int64 `json:"err_no"`
ErrTips string `json:"err_tips"`
}
func (err *TradeError) Error() string {
return fmt.Sprintf("errNo:%d,errTips:%s", err.ErrNo, err.ErrTips)
}
func f() error {
// do something
var err *TradeError
return err
}
func main() {
if err := f(); err != nil {
// do something
fmt.Println("err != nil")
} else {
fmt.Println("err == nil")
}
}
// 永远输出
err != nil
- 对于接口只有值和类型都为nil才会判断为nil
- 如果对任意接口已经赋值,判断接口是为 nil 值的逻辑一定为false 。
47 defer作用时如何计算求值
- 立即对函数的参数求值,而不是在defer后面的语句执行完返回时才计算
func f1() error {
var status string
defer notify(status)
defer incrementCounter(status)
status = StatusSuccess
return nil
}
func notify(status string) {
fmt.Println("notify:", status) // notify:
}
func incrementCounter(status string) {
fmt.Println("increment:", status) // increment:
}
-- 输出
increment:
notify:
// 指针改变
func f2() error {
var status string
defer notifyPtr(&status)
defer incrementCounterPtr(&status)
status = StatusSuccess
return nil
}
--
increment: success
notify: success
func notifyPtr(status *string) {
fmt.Println("notify:", *status)
}
func incrementCounterPtr(status *string) {
fmt.Println("increment:", *status)
}
// 闭包
func f3() error {
var status string
defer func() {
notify(status)
incrementCounter(status)
}()
status = StatusSuccess
return nil
}
--
notify: success
increment: success
61 传递不合时宜的上下文
- 上下文被取消,异步操作可能会意外停止
func TestCancel(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
for i := 0; i < 5; i++ {
go httpGet(ctx)
}
defer cancel()
time.Sleep(time.Second * 3)
}
func httpGet(ctx context.Context) {
// Make a request, that will call the baidu homepage
req, _ := http.NewRequest(http.MethodGet, "http://baidu.com", nil)
// Associate the cancellable context we just created to the request
req = req.WithContext(ctx)
// Create a new HTTP client and execute the request
client := &http.Client{}
res, err := client.Do(req)
// If the request failed, log to STDOUT
if err != nil {
fmt.Println("Request failed:", err) // Request failed: Get "http://baidu.com": context deadline exceeded
return
}
fmt.Println("Response received, status code:", res.StatusCode)
}
62 谨慎使用goroutine和循环变量
// 打印结果无法预估
func listing1() {
s := []int{1, 2, 3}
for _, i := range s {
go func() {
fmt.Print(i)
}()
}
}
// 解决一:使用临时变量
func listing2() {
s := []int{1, 2, 3}
for _, i := range s {
val := i
go func() {
fmt.Print(val)
}()
}
}
// 解决二:传递参数
func listing3() {
s := []int{1, 2, 3}
for _, i := range s {
go func(val int) {
fmt.Print(val)
}(i)
}
}
64 在使用select+channel时期望确定性的结果
- select选择哪个通道是不确定的,会随机选择,不像switch语句是选择第一个匹配的。
package main
import (
"fmt"
"time"
)
func main() {
messageCh := make(chan int, 10)
disconnectCh := make(chan struct{})
go listing1(messageCh, disconnectCh)
for i := 0; i < 10; i++ {
messageCh <- i
}
disconnectCh <- struct{}{}
time.Sleep(10 * time.Millisecond)
}
// 无法保证消息处理完,才断开连接。
func listing1(messageCh <-chan int, disconnectCh chan struct{}) {
for {
select {
case v := <-messageCh:
fmt.Println(v)
case <-disconnectCh:
fmt.Println("disconnection, return")
return
}
}
}
// 1. 强制消费完messageCh 再断开连接
func listing2(messageCh <-chan int, disconnectCh chan struct{}) {
for {
select {
case v := <-messageCh:
fmt.Println(v)
case <-disconnectCh:
for {
select {
case v := <-messageCh:
fmt.Println(v)
default:
fmt.Println("disconnection, return")
return
}
}
}
}
}
// 2. 定义无缓冲的messageCh,发送者会阻塞。
// 3. 只使用一个channel,定义结构体消息,其中一个字段表示数据,另一个字段表示断开连接的标识,
// 只要保证断开连接的消息是最后发送的即可。
65 通知类型通道应使用chan struct{}
通道(channel)是一种通过信号在goroutine之间进行通信的机制。信号可以有数据也可以没有数据。
该通道将在发生特定断开连接时发生通知,一种处理的思路是定义一个chan bool类型的通道,true的含义是断开连接,但是false似乎没有特别的意义,并且应该只会传递ture。那么此时就不需要一个特定的值来传递信息。可以使用chan struct{}类型,它占用的存储空间为零字节。
disconnectCh := make(chan bool)
// 测试 struct{} 占有内存空间
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // 0
比如在go中想使用set,也可以通过定义map[string]struct{}实现。
68 字符串 格式化产生的副作用
- %v 会调用自实现的String()方法
import (
"fmt"
"sync"
)
func main() {
customer := Customer{}
_ = customer.UpdateAge1(-1)
}
type Customer struct {
mutex sync.RWMutex
id string
age int
}
func (c *Customer) UpdateAge1(age int) error {
c.mutex.Lock()
defer c.mutex.Unlock()
if age < 0 {
return fmt.Errorf("age should be positive for customer %v", c) // %v调用String() 导致死锁
}
c.age = age
return nil
}
func (c *Customer) String() string {
c.mutex.RLock()
defer c.mutex.RUnlock()
return fmt.Sprintf("id %s, age %d", c.id, c.age)
}
71 错误使用sync.WaitGroup
- Add操作必须在启动子goroutine之前,在父goroutine中执行完成,而Done操作必须在子goroutine内部执行完成。
// 错误方式
func listing1() {
wg := sync.WaitGroup{}
var v uint64
for i := 0; i < 3; i++ {
go func() {
wg.Add(1)
atomic.AddUint64(&v, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println(v)
}
---
go run -race xxx.go 会捕获到存在数据竞争
---
正确使用方式:
func listing2() {
wg := sync.WaitGroup{}
var v uint64
wg.Add(3)
for i := 0; i < 3; i++ {
go func() {
atomic.AddUint64(&v, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println(v)
}
func listing3() {
wg := sync.WaitGroup{}
var v uint64
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
atomic.AddUint64(&v, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println(v)
}
73 errgroup使用
- 多个goroutine处理时,需要感知其他goroutine存在的错误可以考虑采用errgroup。
例子:
- 第一个调用在执行了1毫秒时返回了错误
- 第二和第三个调用在执行了5秒时返回了结果或错误
在我们的例子中,如果有错误产生,返回一个错误即可,不需要返回所有的错误。所以,这里进行的第二和第三个调用是没有意义的,errgroup将取消上下文,从而取消其他goroutine,因此,我们不用等待后面其他goroutine在5秒后返回的错误,这也是使用errgroup的另一个优势.
func handler2(ctx context.Context, strs []string) ([]string, error) {
results := make([]string, len(strs))
g, ctx := errgroup.WithContext(ctx) // 可以取消上下文
//g := errgroup.Group{} // 无法取消
for i, circle := range strs {
i := i
str := circle
g.Go(func() error {
result, err := foo(ctx, str, i)
if err != nil {
return err
}
results[i] = result
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Println("err:", err)
return nil, fmt.Errorf("handler return err, %w", err)
}
return results, nil
}
func foo(ctx context.Context, str string, i int) (string, error) {
if i == 2 {
return "", fmt.Errorf("i:%d, err", i)
}
time.Sleep(time.Second * 2)
select {
case <-ctx.Done(): // 可以监听到取消 从而退出
return "", nil
default:
fmt.Println("no error")
return str, nil
}
}
func main() {
strings, err := handler2(context.TODO(), []string{"1", "2", "3"})
if err != nil {
fmt.Println("", err.Error())
return
}
fmt.Println(strings)
}
74 不要随意复制sync包中变量
- 变量资源本身带状态且操作要配套的不能拷贝
sync包中不能拷贝的变量
- sync.Cond
- sync.Map
- sync.Mutex
- sync.RWMutex
- sync.Once
- sync.Pool
- sync.WaitGroup
锁的复制有状态
func main() {
type MyMutex struct {
count int
sync.Mutex
}
var mu MyMutex
mu.Lock()
var mu1 = mu //加锁后复制了一个新的Mutex出来,此时 mu1 跟 mu的锁状态一致,都是加锁的状态
mu.count++
mu.Unlock()
mu1.Lock() // 已经是加锁状态重复加锁则报错
mu1.count++
mu1.Unlock()
fmt.Println(mu.count, mu1.count)
}
func main() {
counter := NewCounter()
go func() {
counter.Increment1("foo")
}()
go func() {
counter.Increment1("bar")
}()
time.Sleep(10 * time.Millisecond)
}
type Counter struct {
mu sync.Mutex
counters map[string]int
}
func NewCounter() Counter {
return Counter{counters: map[string]int{}}
}
// 复制sync.Mutex 加的不是同一个锁
func (c Counter) Increment1(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++ // 并发读写错误
}
// go run -race xx.go 存在数据竞争
75 错误的duration时间值
func listing1() {
ticker := time.NewTicker(1000)
for {
select {
case <-ticker.C:
fmt.Println("tick")
}
}
}
func listing2() {
ticker := time.NewTicker(time.Microsecond * 100)
for {
select {
case <-ticker.C:
fmt.Println("tick")
}
}
}
- time.Duration实际上是int64类型的别名,它的单位是纳秒。这里传的是1000纳秒,也就是1微秒。
76 time.After导致内存泄露
比如想实现一段时间内没接收到消息则....的场景
func consumer(ch <-chan Event) {
for {
select {
case event := <-ch:
handle(event)
case <-time.After(time.Hour):
log.Println("warning: no messages received")
}
}
}
上述代码,即使ch通道有消息,在一小时内时一直走event := <-ch分支,但是每次都会运行time.After(time.Hour)而该代码每次申请通道资源大概会消耗200字节的内存。如果每小时收到500万条消息,那会消耗200Byte*5000000=1G的内存空间。
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
建议版本
func consumer3(ch <-chan Event) {
timerDuration := 1 * time.Hour
timer := time.NewTimer(timerDuration)
defer timer.Stop()
for {
timer.Reset(timerDuration)
select {
case event := <-ch:
handle(event)
case <-timer.C:
log.Println("warning: no messages received")
}
}
}
- 在循环中使用time.After并不是唯一可能导致内存泄露的原因,本质原因与重复调用的代码有关。
77 JSON处理常见问题
类型内嵌导致的 序列化 问题
func main() {
type Event1 struct {
ID int
time.Time
}
event := Event1{
ID: 1234,
Time: time.Now(),
}
b, err := json.Marshal(event)
if err != nil {
return
}
fmt.Println(string(b))
return
}
--
"2023-02-02T00:04:28.103983+08:00". ID不见了
---
time.Time其中实现了json.Marshaler接口的MarshalJSON方法,故改变了序列化结果
因单调时钟导致的 序列化 问题 (略)
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
wall和ext共同记录了时间,但是分为两种情况,一种是没有记录单调时钟(比如是通过字符串解析得到的时间),另一种是记录了单调时钟(比如通过Now)。
func main() {
type Event struct {
Time time.Time
}
// 通过time.Now()
t := time.Now()
event1 := Event{
Time: t,
}
b, err := json.Marshal(event1)
if err != nil {
return
}
var event2 Event
err = json.Unmarshal(b, &event2) // 通过序列号生成
if err != nil {
return
}
fmt.Println(event1 == event2)
fmt.Println(event1.Time)
fmt.Println(event2.Time)
return
}
----
false
2023-02-02 00:10:08.881853 +0800 CST m=+0.000104626
------------------------------------ --------------
Wall time Monotonic time
2023-02-02 00:10:08.881853 +0800 CST
---
序列化 数值到map[T]interface{}存在的问题
- 任何数值,当将它通过JSON反序列化到一个map中时,无论数值是否包含小数,都将被转化为float64类型
- 生产代码中真实踩坑记录
func listing1() error {
b := getMessage()
var m map[string]any
err := json.Unmarshal(b, &m)
if err != nil {
return err
}
fmt.Printf("%T\n", m["id"]) // float64
if m["id"] == 32 {
fmt.Println("id is int && id == 32") // 不输出
} else if m["id"] == float64(32) {
fmt.Println("id is float && id == float64(32)") // 输出此项 id is float && id == float64(32)
}
return nil
}
func getMessage() []byte {
str := "{\n "id": 32,\n "name": "foo"\n}"
return []byte(str)
}
func main() {
listing1()
}
79 忘记关闭临时资源
- 未及时调用需要关闭的临时资源,会造成资源泄露。
- 比如http等请求时需要及时调用
close()
func (h handler) getStatusCode(body io.Reader) (int, error) {
resp, err := h.client.Post(h.url, "application/json", body)
if err != nil {
return 0, err
}
defer func() {
err := resp.Body.Close()
if err != nil {
log.Printf("failed to close response: %v\n", err)
}
}()
return resp.StatusCode, nil
}
- 不要将defer写在err检查之前,可能造成panic
func (h handler) getStatusCode(body io.Reader) (int, error) {
resp, err := h.client.Post(h.url, "application/json", body)
// err后resp为nil会造成panic
defer func() {
err := resp.Body.Close()
if err != nil {
log.Printf("failed to close response: %v\n", err)
}
}()
if err != nil {
return 0, err
}
return resp.StatusCode, nil
}