Professional Documents
Culture Documents
Common Go Mistakes Part1
Common Go Mistakes Part1
Common Go Mistakes Part1
Go 编程习惯
目录
代码组织 控制结构
for range 值拷贝
变量遮蔽
for range 指针
init方法
for range map
接口污染
for range defer
内嵌类型
数据类型
数值类型
slice / map 内存问题
值比较
代码组织
变量遮蔽 Variable Shadowing
Bad
if tracing {
if err != nil {
return err
log.Println(client)
} else {
if err != nil {
return err
log.Println(client)
多值短声明赋值,实际重新声明了块内变量,与块外无关
代码组织
变量遮蔽 Variable Shadowing
Recommend
if tracing {
c, err := createClientWithTracing()
if err != nil {
return err
client = c
} else {
// Same logic
块内临时变量,并使用赋值语句
代码组织
变量遮蔽 Variable Shadowing
Recommend
if tracing {
if err != nil {
return err
} else {
// Same logic
块外声明,块内赋值
代码组织
初始化方法 init
package main
import "fmt"
fmt.Println("var")
return 0
}()
func init() {
fmt.Println("init")
func main() {
fmt.Println("main")
}
代码组织
初始化方法 init
tips:
一个文件多个`init`方法
`init`方法没有入参也没有返回,不能产生`error`,除非`panic`
`init`调用顺序
单元测试会引入不必要的初始化
只想调用`init`方法
import _ "foo"
代码组织
初始化方法 init
来自官方 Go Blog 代码
func init() {
http.HandleFunc("/blog", redirect)
http.HandleFunc("/blog/", redirect)
static := http.FileServer(http.Dir("static"))
http.Handle("/favicon.ico", static)
http.Handle("/fonts.css", static)
http.Handle("/fonts/", static)
http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/",
http.HandlerFunc(staticHandler)))
}
代码组织
滥用Getter和Setter
Getter / Setter 的好处
实现校验逻辑,计算逻辑,访问权限(锁)
隐藏了内部的实现,可以灵活决定如何暴露
方便debug,设置断点,监听值的变更
代码组织
接口污染
Go 哲学
Golang 风格之一:简单
代码组织
接口污染
接口应用场景:
相同行为 (Sort.Interface)
type Interface interface {
Len() int
Swap(i, j int)
}
代码组织
接口污染
接口应用场景:
解耦 (里式替换)
代码组织
接口污染
接口应用场景:
限制行为
type IntConfig struct {
// ...
// Retrieve configuration
}
代码组织
接口污染
接口应用场景:
限制行为
type intConfigGetter interface {
Get() int
threshold intConfigGetter
threshold := f.threshold.Get()
// ...
}
代码组织
接口污染
接口的定义应该在消费侧而不是生产侧
定义:生产侧,被导入的包;消费侧,当前包
Go接口特点:
接口的实现具有隐形特性,只要实现了接口内的方法即实现了接口
具体的实现可以脱离接口,实现可以是外部的
观点:
不需要生产侧定义所有接口,强制消费侧接受,而应该由消费侧决定是否需要定义
消费侧应按需设计不同的接口(含有不同的方法,不同的方法组合)
消费侧的接口可以是包内的,不公开的
符合接口隔离原则ISP (Interface-Segregation-Principle):不依赖其不需要的方法
代码组织
接口污染
生产侧定义接口
package foo
println(sth)
Say(string)
package bar
import "foo"
s.Say("Hello")
}
代码组织
接口污染
消费侧定义接口
package foo
println(sth)
package bar
Say(string)
s.Say("Hello")
}
代码组织
接口污染
应该在生产侧定义接口的情况:
应用广泛,可提高代码复用(标准库里的接口)
明确会被用到的,而不是可能
接口定义原则:
尽量小
易于组合
代码组织
接口污染
接口的使用
Be conservative in what you do, be liberal in what you accept from others.
使用原则:
尽量考虑接口作为参数
尽量避免接口作为返回值,应返回具体的结构体
避免循环引用
例外:
package io
return &LimitedReader{r, n}
}
代码组织
接口污染
any say nothing
使用any导致类型信息丢失,需要使用类型断言
使用any导致代码可读性查
使用any不利于静态检查
func main() {
var i any
i = 42
i = "foo"
i = struct {
s string
}{
s: "bar",
i = f
_ = i
代码组织
接口污染
any say nothing
例外
标准库`encoding/json`
// ...
`database/sql`库
// ...
}
代码组织
内嵌类型的可能问题
结构体内嵌
Bar
Baz int
foo := Foo{}
foo.Bar.Baz = 42
foo.Baz = 42 // 提升
代码组织
内嵌类型的可能问题
接口内嵌
Reader
Writer
}
代码组织
内嵌类型的可能问题
type InMem struct {
sync.Mutex
m map[string]int
i.Lock()
v, contains := i.m[key]
i.Unlock()
return v, contains
m := inmem.New()
m.Lock() // ??
代码组织
内嵌类型的可能问题
解决方案:
mu sync.Mutex
m map[string]int
}
代码组织
内嵌类型的可能问题
例外
io.WriteCloser
func main() {
l := Logger{WriteCloser: os.Stdout}
_, _ = l.Write([]byte("foo"))
_ = l.Close()
}
代码组织
内嵌类型的可能问题
使用内嵌类型的约束:
不仅仅是想使用某些语法糖,如字段提升 `foo.Baz`
字段、方法本身就是公开的,而不是私有的,如涉及到内部锁的时候
代码组织
option方法模式
传统思维
Port int
port *int
b.port = &port
return b
// 处理port 相关的判断
// ...
}
代码组织
option方法模式
Go思维
port *int
options.port = &port
return nil
*http.Server, error) {
err := opt(&options)
// ...
// ...
}
代码组织
option方法模式
扩展阅读
GO 编程模式:FUNCTIONAL OPTIONS
陈皓(左耳朵耗子)
专栏:左耳听风
数据类型
数字表现形式
二进制 `0b100`,`0B100`
八进制 `0o100`,`0O100`,`0100`
16进制 `0xF`,`0XF`
虚数 `3i`
`1_000_000_000`
滥用数字进制
fmt.Println(sum)
数据类型
数字类型的溢出
符号类型 无符号类型
counter++
// counter=-2147483648
fmt.Printf("counter=%d\n", counter)
01111111111111111111111111111111
10000000000000000000000000000000
编译期校验有限
if counter == math.MaxInt32 {
panic("int32 overflow")
return counter + 1
panic("int overflow")
return a + b
}
数据类型
数字类型的溢出
防止溢出
if a == 0 || b == 0 {
return 0
result := a * b
if a == 1 || b == 1 {
return result
if a == math.MinInt || b == math.MinInt {
panic("integer overflow")
if result/b != a {
panic("integer overflow")
return result
}
数据类型
关于浮点型计算
var n float32 = 1.0001
fmt.Println(n * n)
输出?
1.0002
数据类型
关于浮点型计算
func f1(n int) float64 {
result := 10_000.
result += 1.0001
return result
result := 0.
result += 1.0001
}
数据类型
关于浮点型计算
n = 10:
ev 10010.001
f1 10010.000999999993
f2 10010.001
_______________
:
n = 1k
ev 11000.1
f1 11000.099999999293
f2 11000.099999999982
_______________
:
n = 1m
ev 1.0101e+06
f1 1.0100999999761417e+06
f2 1.0100999999766762e+06
结论
计算次数越多,越不精确
计算顺序不同,结果不同
数据类型
slice的长度和容量
一个将`[]Foo`类型转换为`[]Bar`类型的方法
bars := make([]Bar, 0)
return bars
如何优化?
数据类型
slice的长度和容量
提前分配容量,避免反复扩容,浪费内存
n := len(foos)
bars := make([]Bar, 0, n)
return bars
进一步优化?
数据类型
slice的长度和容量
直接定义长度,并使用索引赋值
n := len(foos)
bars := make([]Bar, n)
bars[i] = fooToBar(foo)
return bars
}
数据类型
slice的长度和容量
benchmark
func main() {
var s []string
log("var s []string\t\t", 1, s)
s = []string(nil)
log("s = []string(nil)\t", 2, s)
s = []string{}
log("s = []string{}\t\t", 3, s)
s = make([]string, 0)
}
数据类型
slice 是 nil 还是 空
var s []string 1: empty=true nil=true
无论是否nil,都可以使用append追加数据
nil slice 相对于长度为0的非nil slice,不需要额外分配内存
方法的返回值推荐返回`nil` slice,而不一定要空`[]T{}`
选择性的接受第三条
数据类型
slice 是 nil 还是 空
非 空
`json.Marshal` 对于 `nil map` 和 ` nil map` 的序列化结果不同
var s1 []float32
customer1 := customer{
ID: "foo",
Operations: s1,
b, _ := json.Marshal(customer1)
fmt.Println(string(b))
s2 := make([]float32, 0)
customer2 := customer{
ID: "bar",
Operations: s2,
b, _ = json.Marshal(customer2)
fmt.Println(string(b))
// console{"ID":"foo","Operations":null}
// {"ID":"bar","Operations":[]}
数据类型
slice 是 nil 还是 空
tips:
copy(dst, src)
fmt.Println(dst)
输出?
[]
原因是`dst`只有声明,即`nil`,没有初始化,所以copy了0个元素,dst仍然是`nil`
// 或者
s2 := s1[1:2]
s3 := append(s2, 10)
fmt.Println("s1", s1)
fmt.Println("s2", s2)
fmt.Println("s3", s3)
输出?
s1 [1 2 10]
s2 [2]
s3 [2 10]
数据类型
slice append 副作用
s1、s2、s3 三个slice共享底层数组
s1 := []int{1, 2, 3}
s2 := s1[1:2]
for {
msg := receiveMessage()
storeMessageType(getMessageType(msg))
return msg[:5]
}
哪里泄露?
数据类型
slice 内存泄露
func consumeMessages() {
for {
msg := receiveMessage()
storeMessageType(getMessageType(msg))
return msg[:5]
}
数据类型
slice 内存泄露
原因:
直接截取slice片段,导致msg底层数组没有释放
解决方式:
还是 `copy`
msgType := make([]byte, 5)
copy(msgType, msg)
return msgType
}
数据类型
低效的 map
tips:
初始化map时,尽可能指定length,避免扩容时创建bucket及再平衡(rebalancing)
大量删除map元素时,map占用的内存不会显著释放(被删除的元素内存释放),但是map底层的`bucket`
不会减少,内存不会释放
如何应对这种大量新增、删除map元素的场景?
数据类型
低效的 map
方案一:定时将原map拷贝到新的map,然后将原map删除
不良的后果是,最坏的情况,复制完但还没有GC,内存占用是原map内存占用峰值的两倍
`bucket` 仍然不会减少,但因为存储的是指针,内存占用明显会减少
操作 `map[int][128]byte` `map[int]*[128]byte` 比例
声明空map 0 MB 0 MB 1
布尔值
数值(int,float,复数)
字符串
结构体(不包含map、slice、方法)
数组(不包含map、slice、方法)
chan(值相等或为nil)
接口类型(具体数据结构不包含map、slice、方法,值相等或为nil)
指针类型(值相等或为nil)
数据类型
值比较
效率低的做法
reflect.DeepEqual
数组中的所有元素都深度相等(DeepEqual)
结构体中的所有字段,包括公开和私有的,都深度相等
方法都是nil
接口具体类型深度相等
map都是nil,或长度相等且键值对深度相等
指针相同,或者指针指向的值深度相等
slice都是nil,或同样长度且指向相同的底层数组(指向的入口值也相同),或各元素深度相等
https://pkg.go.dev/reflect#DeepEqual
数据类型
值比较
// slice
s1 := []int{1, 2, 3}
s2 := []int{3, 2, 1}
s3 := []int{1, 2, 3}
s4 := s1[1:]
s5 := s1[1:]
fmt.Println(reflect.DeepEqual(s1, s2))
fmt.Println(reflect.DeepEqual(s1, s3))
fmt.Println(reflect.DeepEqual(s4, s5))
// map
fmt.Println(reflect.DeepEqual(m1, m2))
// pointer
p1 := &[...]int{1, 2, 3}
p2 := &[...]int{1, 2, 3}
p3 := p1
fmt.Println(p1 == p2)
fmt.Println(reflect.DeepEqual(p1, p2))
fmt.Println(reflect.DeepEqual(p1, p3))
控制结构
for range 的值拷贝
accounts := []account{
{balance: 100.},
{balance: 200.},
{balance: 300.},
a.balance += 1000
fmt.Println(accounts)
输出?
[{100} {200} {300}]
控制结构
for range 的值拷贝
s := []int{0, 1, 2}
for range s {
s = append(s, 10)
这个会不会一致迭代下去?
s := []int{0, 1, 2}
s = append(s, 10)
}
控制结构
for range 的值拷贝
ch1 := make(chan int, 3)
go func() {
ch1 <- 0
ch1 <- 1
ch1 <- 2
close(ch1)
}()
go func() {
ch2 <- 10
ch2 <- 11
ch2 <- 12
close(ch2)
}()
ch := ch1
for v := range ch {
fmt.Println(v)
ch = ch2
}
控制结构
for range 的值拷贝
a := [3]int{0, 1, 2}
for i, v := range a {
a[2] = 10
if i == 2 {
fmt.Println(v)
fmt.Println(a)
输出?
[0 1 10]
控制结构
for range 的值拷贝
解决方案:
a := [3]int{0, 1, 2}
a[2] = 10
if i == 2 {
fmt.Println(a[2])
a[2] = 10
if i == 2 {
fmt.Println(v)
ID string
Balance float64
m map[string]*Customer
s.m[customer.ID] = &customer
ID: 1, Balance: {3 0}
ID: 2, Balance: {3 0}
ID: 3, Balance: {3 0}
控制结构
for range 使用指针的危害
修复方式:
// 使用局部变量
current := customer
s.m[current.ID] = ¤t
// 使用索引
customer := &customers[i]
s.m[customer.ID] = customer
}
控制结构
for range map 的插入
m := map[int]bool{
0: true,
1: false,
2: true,
for k, v := range m {
if v {
m[10+k] = true
fmt.Println(m)
输出?
执行多次,输出结果各不相同:
The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If a map entry that
has not yet been reached is removed during iteration, the corresponding iteration value will not be produced. If a map entry is created
during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and
from one iteration to the next. If the map is nil, the number of iterations is 0.
https://go.dev/ref/spec
map迭代顺序无法保证,不可以依赖map初始化或者插入的顺序
如果迭代过程中删除一个元素,如果该值还没有被迭代到,则不会产生相应的迭代值
如果迭代过程中添加一个元素,则可能产生迭代值,也可能会被跳过,且对于插入的元素每次迭代可能有不同
的选择
如果map是nil,则迭代数量为0
控制结构
for range map 的插入
解决方案:
依旧是 `copy` 思想
m := map[int]bool{
0: true,
1: false,
2: true,
m2 := copyMap(m)
for k, v := range m {
m2[k] = v
if v {
m2[10+k] = true
fmt.Println(m2)
控制结构
for range 循环内部 defer
func readFiles(ch <-chan string) error {
if err != nil {
return err
defer file.Close()
// 对文件做一些其他操作
return nil
上面的代码可能存在什么问题?
控制结构
for range 循环内部 defer
`readFiles` 长时间未关闭,可能导致 ` 文件句柄` 泄露,`fd`耗尽,无法打开新的文件,产生 `too many open
files` 报错
return err
return nil
if err != nil {
return err
defer file.Close()
// ...
return nil
}
下一步
第二期 第三期
字符串 标准库
函数/方法 测试
error管理 优化
并发