Common Go Mistakes Part1

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 71

Common Go Mistakes

Go 编程习惯
目录
代码组织 控制结构
for range 值拷贝
变量遮蔽
for range 指针
init方法
for range map
接口污染
for range defer
内嵌类型

数据类型

数值类型
slice / map 内存问题
值比较
代码组织
变量遮蔽 Variable Shadowing
Bad

var client *http.Client

if tracing {

client, err := createClientWithTracing()

if err != nil {

return err

log.Println(client)

} else {

client, err := createDefaultClient()

if err != nil {

return err

log.Println(client)

多值短声明赋值,实际重新声明了块内变量,与块外无关
代码组织
变量遮蔽 Variable Shadowing
Recommend

var client *http.Client

if tracing {

c, err := createClientWithTracing()

if err != nil {

return err

client = c

} else {

// Same logic

块内临时变量,并使用赋值语句
代码组织
变量遮蔽 Variable Shadowing
Recommend

var client *http.Client

var err error

if tracing {

client, err = createClientWithTracing()

if err != nil {

return err

} else {

// Same logic

块外声明,块内赋值
代码组织
初始化方法 init
package main

import "fmt"

var a = func() int {

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() {

redirect := func(w http.ResponseWriter, r *http.Request) {

http.Redirect(w, r, "/", http.StatusFound)

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 哲学

The bigger the interface, the weaker the abstraction.


Don’t design with interfaces, discover them.
—Rob Pike

Golang 风格之一:简单
代码组织
接口污染
接口应用场景:

相同行为 (Sort.Interface)
type Interface interface {

Len() int

Less(i, j int) bool

Swap(i, j int)
}
代码组织
接口污染
接口应用场景:

解耦 (里式替换)
代码组织
接口污染
接口应用场景:

限制行为
type IntConfig struct {

// ...

func (c *IntConfig) Get() int {

// Retrieve configuration

func (c *IntConfig) Set(value int) {


// Update configuration

}
代码组织
接口污染
接口应用场景:

限制行为
type intConfigGetter interface {

Get() int

type Foo struct {

threshold intConfigGetter

func NewFoo(threshold intConfigGetter) Foo {

return Foo{threshold: threshold}

func (f Foo) Bar() {

threshold := f.threshold.Get()

// ...

}
代码组织
接口污染
接口的定义应该在消费侧而不是生产侧

定义:生产侧,被导入的包;消费侧,当前包

Go接口特点:

接口的实现具有隐形特性,只要实现了接口内的方法即实现了接口
具体的实现可以脱离接口,实现可以是外部的

观点:

不需要生产侧定义所有接口,强制消费侧接受,而应该由消费侧决定是否需要定义
消费侧应按需设计不同的接口(含有不同的方法,不同的方法组合)
消费侧的接口可以是包内的,不公开的
符合接口隔离原则ISP (Interface-Segregation-Principle):不依赖其不需要的方法
代码组织
接口污染
生产侧定义接口

package foo

type Human struct{}

func (h Human) Say(sth string) {

println(sth)

type Say interface {

Say(string)

package bar

import "foo"

func Baz(s foo.Say) {

s.Say("Hello")
}
代码组织
接口污染
消费侧定义接口

package foo

type Human struct{}

func (h Human) Say(sth string) {

println(sth)

package bar

type say interface {

Say(string)

func Baz(s say) {

s.Say("Hello")
}
代码组织
接口污染
应该在生产侧定义接口的情况:

应用广泛,可提高代码复用(标准库里的接口)
明确会被用到的,而不是可能

接口定义原则:

尽量小
易于组合
代码组织
接口污染
接口的使用

Be conservative in what you do, be liberal in what you accept from others.

使用原则:

尽量考虑接口作为参数
尽量避免接口作为返回值,应返回具体的结构体
避免循环引用

例外:

package io

func LimitReader(r Reader, n int64) Reader {

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`

func Marshal(v any) ([]byte, error) {

// ...

`database/sql`库

func (c *Conn) QueryContext(ctx context.Context, query string,

args ...any) (*Rows, error) {

// ...

}
代码组织
内嵌类型的可能问题
结构体内嵌

type Foo struct {

Bar

type Bar struct {

Baz int

foo := Foo{}

foo.Bar.Baz = 42

foo.Baz = 42 // 提升
代码组织
内嵌类型的可能问题
接口内嵌

type ReadWriter interface {

Reader

Writer

}
代码组织
内嵌类型的可能问题
type InMem struct {

sync.Mutex

m map[string]int

func New() *InMem {

return &InMem{m: make(map[string]int)}

func (i *InMem) Get(key string) (int, bool) {

i.Lock()

v, contains := i.m[key]

i.Unlock()

return v, contains

m := inmem.New()

m.Lock() // ??
代码组织
内嵌类型的可能问题
解决方案:

type InMem struct {

mu sync.Mutex

m map[string]int

}
代码组织
内嵌类型的可能问题
例外

type Logger struct {

io.WriteCloser

func main() {

l := Logger{WriteCloser: os.Stdout}

_, _ = l.Write([]byte("foo"))

_ = l.Close()

}
代码组织
内嵌类型的可能问题
使用内嵌类型的约束:

不仅仅是想使用某些语法糖,如字段提升 `foo.Baz`
字段、方法本身就是公开的,而不是私有的,如涉及到内部锁的时候
代码组织
option方法模式
传统思维

type Config struct {

Port int

type ConfigBuilder struct {

port *int

func (b *ConfigBuilder) Port(port int) *ConfigBuilder {

b.port = &port

return b

func (b *ConfigBuilder) Build() (Config, error) {

// 处理port 相关的判断

return cfg, nil

func NewServer(addr string, config Config) (*http.Server, error) {

// ...

}
代码组织
option方法模式
Go思维

type options struct {

port *int

type Option func(options *options) error

func WithPort(port int) Option {

return func(options *options) error {

options.port = &port

return nil

func NewServer(addr string, opts ...Option) (

*http.Server, error) {

var options options

for _, opt := range opts {

err := opt(&options)

// ...

// ...

}
代码组织
option方法模式
扩展阅读

GO 编程模式:FUNCTIONAL OPTIONS

陈皓(左耳朵耗子)

专栏:左耳听风
数据类型
数字表现形式
二进制 `0b100`,`0B100`
八进制 `0o100`,`0O100`,`0100`
16进制 `0xF`,`0XF`
虚数 `3i`
`1_000_000_000`

file, err := os.OpenFile("foo", os.O_RDONLY, 0644)

file, err := os.OpenFile("foo", os.O_RDONLY, 0o644)

滥用数字进制

sum := 100 + 010

fmt.Println(sum)
数据类型
数字类型的溢出
符号类型 无符号类型

int8 (8 bits) uint8 (8 bits)

int16 (16 bits) uint16 (16 bits)

int32 (32 bits) uint32 (32 bits)

int64 (64 bits) uint64 (64 bits)

int (? bits) uint (? bits)


数据类型
数字类型的溢出
var counter int32 = math.MaxInt32

counter++
// counter=-2147483648

fmt.Printf("counter=%d\n", counter)

01111111111111111111111111111111

|------31 bits set to 1-------|

10000000000000000000000000000000

|------31 bits set to 0-------|

编译期校验有限

var counter int32 = math.MaxInt32 + 1

// constant 2147483648 overflows int32


数据类型
数字类型的溢出
防止溢出

func Inc32(counter int32) int32 {

if counter == math.MaxInt32 {

panic("int32 overflow")

return counter + 1

func AddInt(a, b int) int {


if a > math.MaxInt-b {

panic("int overflow")

return a + b

}
数据类型
数字类型的溢出
防止溢出

func MultiplyInt(a, b int) int {

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.

for i := 0; i < n; i++ {

result += 1.0001

return result

func f2(n int) float64 {

result := 0.

for i := 0; i < n; i++ {

result += 1.0001

return result + 10_000.

}
数据类型
关于浮点型计算
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`类型的方法

func convert(foos []Foo) []Bar {

bars := make([]Bar, 0)

for _, foo := range foos {

bars = append(bars, fooToBar(foo))

return bars

如何优化?
数据类型
slice的长度和容量
提前分配容量,避免反复扩容,浪费内存

func convert(foos []Foo) []Bar {

n := len(foos)

bars := make([]Bar, 0, n)

for _, foo := range foos {

bars = append(bars, fooToBar(foo))

return bars

进一步优化?
数据类型
slice的长度和容量
直接定义长度,并使用索引赋值

func convert(foos []Foo) []Bar {

n := len(foos)

bars := make([]Bar, n)

for i, foo := range foos {

bars[i] = fooToBar(foo)

return bars

}
数据类型
slice的长度和容量
benchmark

BenchmarkConvert_EmptySlice-4 22 49739882 ns/op

BenchmarkConvert_GivenCapacity-4 86 13438544 ns/op

BenchmarkConvert_GivenLength-4 91 12800411 ns/op


数据类型
slice 是 nil 还是 空
func main() {

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)

log("s = make([]string, 0)", 4, s)

func log(expr string, i int, s []string) {

fmt.Printf("%s\t%d: empty=%t\tnil=%t\n", expr, i, len(s) == 0, s == nil)

}
数据类型
slice 是 nil 还是 空
var s []string 1: empty=true nil=true

s = []string(nil) 2: empty=true nil=true

s = []string{} 3: empty=true nil=false

s = make([]string, 0) 4: empty=true nil=false

无论是否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:

如果不确定slice的最终长度或者不确定是否可以为空,那么使用 `var s []string` 这样的声明


使用 `[]string(nil)` 这样的语法糖来创建nil和空slice
如果确定最终的长度,使用 `make([]string, length)`(为什么不直接使用数组?)
尽量使用 ` 长度` 判断是否为空,而非 `nil`
数据类型
slice 复制
src := []int{0, 1, 2}

var dst []int

copy(dst, src)

fmt.Println(dst)

输出?

[]

原因是`dst`只有声明,即`nil`,没有初始化,所以copy了0个元素,dst仍然是`nil`

dst := make([]int, len(src))

// 或者

dst := append([]int{}, src...)


数据类型
slice append 副作用
s1 := []int{1, 2, 3}

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]

s3 := append(s2, 10, 11)

安全起见,`s2` 使用 `copy` 复制元素


数据类型
slice 内存泄露
func consumeMessages() {

for {
msg := receiveMessage()

// Do something with msg

storeMessageType(getMessageType(msg))

func getMessageType(msg []byte) []byte {

return msg[:5]
}

哪里泄露?
数据类型
slice 内存泄露
func consumeMessages() {

for {
msg := receiveMessage()

// Do something with msg

storeMessageType(getMessageType(msg))

func getMessageType(msg []byte) []byte {

return msg[:5]
}
数据类型
slice 内存泄露
原因:

直接截取slice片段,导致msg底层数组没有释放

解决方式:

还是 `copy`

func getMessageType(msg []byte) []byte {

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内存占用峰值的两倍

方案二:map值使用指针类型,如 `map[int][128]byte` 改为 `map[int]*[128]byte`

`bucket` 仍然不会减少,但因为存储的是指针,内存占用明显会减少

操作 `map[int][128]byte` `map[int]*[128]byte` 比例

声明空map 0 MB 0 MB 1

添加100万元素 461 MB 182 MB 39.5%

删除100万元素 293 MB 38 MB 13%


数据类型
值比较
可比较的类型:

布尔值
数值(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

m1 := map[int]string{1: "a", 2: "b", 3: "c"}

m2 := map[int]string{2: "b", 3: "c", 1: "a"}

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.},

for _, a := range accounts {

a.balance += 1000

fmt.Println(accounts)

输出?
[{100} {200} {300}]
控制结构
for range 的值拷贝
s := []int{0, 1, 2}

for range s {

s = append(s, 10)

这个会不会一致迭代下去?

`range` 拷贝了 slice 的结构,因此只迭代了初始结构的`len`次,append的数据并不在迭代范围


控制结构
for range 的值拷贝
下面这段代码

s := []int{0, 1, 2}

for i := 0; i < len(s); i++ {

s = append(s, 10)

}
控制结构
for range 的值拷贝
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

}
控制结构
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}

for i := range a { // 使用索引

a[2] = 10

if i == 2 {

fmt.Println(a[2])

for i, v := range &a { // 使用数组的指针

a[2] = 10

if i == 2 {

fmt.Println(v)

如果是切片 `a := []int{0, 1, 2}` 呢?


控制结构
for range 使用指针的危害
type Customer struct {

ID string

Balance float64

type Store struct {

m map[string]*Customer

func (s *Store) storeCustomers(customers []Customer) {

for _, customer := range customers {

s.m[customer.ID] = &customer

执行 `storeCustomers` 后 `s.m` 的内容?

ID: 1, Balance: {3 0}

ID: 2, Balance: {3 0}

ID: 3, Balance: {3 0}
控制结构
for range 使用指针的危害
修复方式:

// 使用局部变量

func (s *Store) storeCustomers(customers []Customer) {

for _, customer := range customers {

current := customer

s.m[current.ID] = &current

// 使用索引

func (s *Store) storeCustomers(customers []Customer) {

for i := range customers {

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)

输出?

执行多次,输出结果各不相同:

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]


控制结构
for range map 的插入

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 {

for path := range ch {

file, err := os.Open(path)

if err != nil {

return err

defer file.Close()

// 对文件做一些其他操作

return nil

上面的代码可能存在什么问题?
控制结构
for range 循环内部 defer
`readFiles` 长时间未关闭,可能导致 ` 文件句柄` 泄露,`fd`耗尽,无法打开新的文件,产生 `too many open
files` 报错

func readFiles(ch <-chan string) error {

for path := range ch {

if err := readFile(path); err != nil { // 封装为单独的方法,或者闭包

return err

return nil

func readFile(path string) error {

file, err := os.Open(path)

if err != nil {

return err

defer file.Close()

// ...

return nil

}
下一步
第二期 第三期
字符串 标准库
函数/方法 测试
error管理 优化
并发

You might also like