背景介绍
Go is simple but not easy.Go 很简单,但不容易掌握
type: 类型系统
先说结论:
- 用法: 类型声明 declare; 类型转换 trans; 类型别名 alias; 类型断言 assert
- 值类型 vs 指针类型
- 0值可用
Go内置关键字, 大部分可以直接从源码中查看 /src/builtin/builtin.go
中查看, 其中大部分都是Go内置类型
怎么快速查看Go源码: https://developer.aliyun.com/article/1137871
var 变量
变量的本质: 特定名字 <-> 特定内存块
- 静态语言: 变量所绑定的**内存**区域是要有一个明确的边界的 -> 知道类型才能知道大小
- 指针: 指针虽然大小固定(32bit/64bit, 依赖平台), 但是其指向的内存, 必须知道类型, 才能知道大小
变量声明的3种方式:
:=
推荐, 支持类型自动推导, 常用分支控制中的局部变量var
- 函数的命名返回值
// 申明且显式初始化a :=int(10) // 默认为0值varaintfuncA() (aint) // 命名返回值相当于 var a int
变量的作用域(scope)
- 包级别变量: 大写可导出
- 局部变量: 代码库(block
{}
) 控制语句(for if switch)
变量常见问题 - 变量遮蔽(variable shadowing): 定义了同名变量, 容易导致变量混淆, 产生隐藏bug且难以定位
a, err :=A() // do somethingb, err :=B() // 再次定义同名 err 变量
type alias 类型别名
类型别名(type alias)的存在,是 渐进式代码修复(Gradual code repair) 的关键
// <Go>/src/builtin/builtin.go// rune is an alias for int32 and is equivalent to int32 in all ways. It is// used, by convention, to distinguish character values from integer values.typerune=int32
类型别名其实是对现实世界的一种映射, 同一个事物拥有不同的名字的场景太多, 比如 apple
和 苹果
, 再比如 土豆
和 马铃薯
, 更有意思的一个例子:
你们抓周树人,关我鲁迅什么事? -- 《楼外楼》
0值
Go中基础类型和0值对照表:
td {white-space:pre-wrap;border:1px solid #dee0e3;}
type | 0值 |
int byte rune | 0 |
float | 0 |
bool | 'false |
string | "" |
struct | 字段都为0值 |
slice map pointer interface func | nil |
关于 nil, 可以从源码中获取到详细信息:
// <Go>/src/builtin/builtin.go// nil is a predeclared identifier representing the zero value for a// pointer, channel, func, interface, map, or slice type.varnilType// Type must be a pointer, channel, func, interface, map, or slice type
func
也只是类型的一种:
t :=T{} f :=func(){} // 函数字面值.FunctionLiteraltypeHandlerFuncfunc(ResponseWriter, *Request) http.HandlerFunc(hello) // hello 和 HandlerFunc 出入参相同, 所以才能进行类型转换funchello(writerhttp.ResponseWriter, request*http.Request) { // fmt.Fprintln(writer, "<h1>hello world</h1>")fmt.Fprintf(writer, "<h1>hello world %v</h1>", request.FormValue("name")) }
值类型 vs 指针类型
结合上面变量的本质来理解:
变量的本质: 特定名字 <-> 特定内存块
那么值类型和指针类型就很容易理解: 值类型在函数调用过程中会发生复制, 指向新的内存块, 而指针则指向同一块内存
再结合上面的0值, 有一个简单的规则:
0值的为 nil 的类型, 函数调用时不会发生复制
当然, 这条规则还需要打上不少补丁, 我们在后面继续聊
还有一个经典问题: 值类型 vs 指针类型, 怎么选 / 用哪个?
其实回答这个问题, 只需要列举几个 Must
的 case 即可:
noCopy
: 不应该复制的场景, 这种情况必须使用指针类型, 尤其要注意 struct, 默认是值类型T
, 如果有noCopy
字段, 必须使用指针类型*T
// 源码中 sync.Mutex 上的说明// A Mutex must not be copied after first use.// Go中还有特殊 noCopy 类型// noCopy may be added to structs which must not be copied// after the first use.//// See https://golang.org/issues/8005#issuecomment-190753527// for details.//// Note that it must not be embedded, due to the Lock and Unlock methods.typenoCopystruct{} // Lock is a no-op used by -copylocks checker from `go vet`.func (*noCopy) Lock() {} func (*noCopy) Unlock() {}
- 不应当复制的场景: 比如结构体使用
[]byte
字段, 如果使用值类型T
导致[]byte
在调用过程中产生复制, 会大大影响性能, 这种情况就要使用*T
, 更多细节, 可以参考这个地址: 03 Decisions | Google Style Guides (gocn.github.io)
// Good:typeRecordstruct { bufbytes.Buffer// other fields omitted} funcNew() *Record {...} func (r*Record) Process(...) {...} funcConsumer(r*Record) {...} // Bad:typeRecordstruct { bufbytes.Buffer// other fields omitted} func (rRecord) Process(...) {...} // Makes a copy of r.buffuncConsumer(rRecord) {...} // Makes a copy of r.buf
0值可用
大部分情况下, Go中的类型都是满足 0值可用
的, 需要注意几个点:
map
不是0值可用
, 必须进行初始化
- 使用 make, 如果知道大小也可以预先指定
- 初始化对应值
- 函数命名返回值中的map
(m map[int]int
, 需要显式初始化一次
m :=make(map[int]int, 10) // 推荐varm=map[int]int{1:1} // 初始化对应值
- 0值可用的特殊类型:
sync.Mutex
sync.Once
...
// 以 sync.Mutex 的使用举例varmusync.Mutex// 零值不需要额外初始化typeCounterstruct { TypeintNamestringmusync.Mutex// 1.放在要控制的字段上面并空行 2.内嵌字段cntuint64} // 1.封装成方法 // 2.读写都需要func (c*Counter) Incr() { c.mu.Lock() c.cnt++c.mu.Unlock() } func (c*Counter) Cnt() uint64 { c.mu.Lock() deferc.mu.Unlock() returnc.cnt}
- 在具体实践过程中, 类型在申明时没有赋值会自动赋0值, 就需要注意0值什么满足业务需求, 比如:
typeReqListReqstruct { Monthstring`form:"month"`// 期间, 格式: 202212Statuspay.Status`form:"status"`// 审批状态} typeStatusint// 审批状态const ( StatusNoneStatus=iota// 0值StatusInit// 未开始StatusIng// 审批中StatusDone// 已通过StatusReject// 已拒绝StatusCancel// 已撤回)
如果请求带了 status
查询条件, 则一定非0值
语法和易错点
byte / rune / string
type rune = int32
: Go中用 rune 表示一个 utf8 编码, 在 utf8 中, 一个字符由 1-4字节
来编码
len("汉") // 3utf8.RuneCountInString("汉") // 1[]byte("汉") // []byte{0xE6, 0xB1, 0x89}[]rune("汉")
遍历string:
for-i
/s[0]
-> bytefor-range
-> rune
字符串拼接:
+ +=
fmt
- strings 或者 bytes 包中的方法:
strings.Builder
性能上的注意点:
string 和 []byte
的转换会分配内存, 一个推荐的做法是[]byte
使用 bytes 包中的方法, 基本 strings 包有的功能, bytes 包都有- 使用 strings.Builder 时一定要使用
Grow()
, 底层是使用的 slice
slice
- 预先指定大小, 减少内存分配
len
需要指定为 0, len不为0时会将len
个元素全部设为0值,append
从len
后的元素开始
s :=make([]int, 0, 10) s=append(s, 10)
- 切片的切片: slice 底层使用的
array
, 由于 slice 会自动扩容, 在使用切片的切片时, 就一定要小心: 发生写操作时, 是否会影响到原来的切片?
map
- map不是0值可用, 上面👆🏻已经讲到
- map是无序的, 而且是开发组特意加的, 原因可以参考官方blog, 这一条说起来简单, 但是实践上却非常容易犯错, 特别是使用 map 返回 keys / values 集合的情况
- 查询使用的下拉框
- 查询多行数据后使用 map 拼接数据, 然后使用map返回 values
- 解决map无序通常2个方法
- 使用slice保证顺序: 比如上面的例子, 申明了个 slice 就好了, 因为元素都是指针, 让map去拼数据, 后续返回的 slice 就是最终结果了
- 使用
sort.Slice
排序
- map无序还会影响一个骚操作:
for-range
遍历map的时候新增key, 新增的key不一定会被遍历到
sort.Slice(resp, func(i, jint) bool { returnresp[i].MonthNumber<resp[j].MonthNumber})
- map没有使用
ok
进行判断, 尤其是map[k]*T
的场景, 极易导致runtime error: invalid memory address or nil pointer dereference
- map不是并发安全, 真这样写了, 编译也不会通过的😅
- map实现
set
, 推荐使用struct{}
明确表示不需要value
typeSet[Kcomparable] map[K]struct{}
struct
- 最重要的其实上面已经介绍过的:
T
是值类型,*T
是指针类型
T
在初始化时默认会把所有字段设为为0值*T
默认是nil, 其实是不可用状态, 必须要初始化后才能使用- 值类型
T
会产生复制, 要注意noCopy
的场景
vartT// T的所有字段设置为0值进行初始化vart*T// nil, 不推荐, t必须初始化才能使用(t*T) // 函数的命名返回值也会踩这个坑t :=&T{} // 等价的, 都是使用 0 值来初始化T并返回指针, 推荐使用 &T{}t :=new(T)
还有2个奇淫巧技
struct{}
是 0 内存占用, 可以在一些优化一些场景, 不需要分配内存, 比如
- 上面的
type Set[K comparable] map[K]struct{}
- chan:
chan struct{}
- struct 内存对齐(aligned)
// 查看内存占用: unsafe.Sizeofi :=int32(10) s :=struct {}{} fmt.Println(unsafe.Sizeof(i)) // 4fmt.Println(unsafe.Sizeof(s)) // 0// 查看内存对齐后的内存占用unsafe.Alignof()
for
for-range
循环, 循环的 v 始终指向同一个内存, 每次都讲遍历的元素, 进行值复制给 v
// badvara []Tvarb []*Tfor_, v :=rangea { b=append(b, &v) // &V 都是指向同一个地址, 最后导致 b 中都是相同的元素}
for+go
外部变量 vs 传参
// badfori :=0; i<10; i++ { gofunc() { println(i) // 协程被调度时, i 的值并不确定 }() } // goodfori :=0; i<10; i++ { gofunc(iint) { println(i) }(i) }
break
break + for/select/switch 只能跳出一层循环, 如果要跳出多层循环, 使用 break label
switch
Go中的switch和以前的语言有很大的不同, break只能退出当前switch, 而 Go 中 switch 执行完当前 case 就会退出, 所以大部分情况下, break 都可以省略
func
- 出入参: 还是上面的内容,
值类型 vs 指针类型
, 需要注意的是:string/slice/map
作为入参, 只是传了一个描述符进来, 并不会发生全部数据的拷贝 - 变长参数:
func(a ...int)
相当于a []int
- 具名返回值
(a int)
相当于var a int
, 考虑到 0值可用, 一定要注意是否要对变量进行初始化
- 适用场景: 相同类型的值进行区分, 比如返回经纬度; 简短函数简化0值申明, 是函数更简洁
- func也是一种类型,
var f func()
和func f()
函数签名相同(出入参相同)时可以进行类型转换
err
- 惯例
- 如果有 err, 作为函数最后一个返回值
- 预期内err, 使用 value; 非预期err, 使用 type
// 初始化err :=errors.New("xxx") err :=fmt.Errorf("%v", xxx) // wraperr :=fmt.Errorf("wrap err: %w", err) // 预期内errvarErrFoo=errors.New("foo") // 非预期err, 比如 net.Error// An Error represents a network error.typeErrorinterface { errorTimeout() bool// Is the error a timeout?// Deprecated: Temporary errors are not well-defined.// Most "temporary" errors are timeouts, and the few exceptions are surprising.// Do not use this method.Temporary() bool}
- 推荐使用
pkg/errors
, 使用%v
可以查看err信息, 使用%+v
可以查看调用栈
- 原理是实现了
type Formatter interface
// Format formats the frame according to the fmt.Formatter interface.//// %s source file// %d source line// %n function name// %v equivalent to %s:%d//// Format accepts flags that alter the printing of some verbs, as follows://// %+s function name and path of source file relative to the compile time// GOPATH separated by \n\t (<funcname>\n\t<path>)// %+v equivalent to %+s:%dfunc (fFrame) Format(sfmt.State, verbrune) { switchverb { case's': switch { cases.Flag('+'): io.WriteString(s, f.name()) io.WriteString(s, "\n\t") io.WriteString(s, f.file()) default: io.WriteString(s, path.Base(f.file())) } case'd': io.WriteString(s, strconv.Itoa(f.line())) case'n': io.WriteString(s, funcname(f.name())) case'v': f.Format(s, 's') io.WriteString(s, ":") f.Format(s, 'd') } }
defer
- 性能: Go1.17 优化过, 性能损失<5% -> 放心使用
- 场景
- 关闭资源:
defer conn.Close()
- 配套使用的函数:
defer mu.Unlock()
recover()
- 上面场景以外的骚操作, 需谨慎编码
panic
运行时 / panic()
产生, panic 会一直出栈, 直到程序退出或者 recover, 而 defer 一定会在函数运行后执行, 所以:
recover()
必须放在 defer 中执行, 保证能捕捉到 panic- 当前协程的 panic 只能被当前协程的 recover 捕获, 一定要小心 野生goroutine, 详细参考这篇blog:
Go源码中还有一种用法: 提示潜在bug
// json/encode.go resolve()func (w*reflectWithString) resolve() error { ifw.k.Kind() ==reflect.String { w.ks=w.k.String() returnnil } iftm, ok :=w.k.Interface().(encoding.TextMarshaler); ok { ifw.k.Kind() ==reflect.Pointer&&w.k.IsNil() { returnnil } buf, err :=tm.MarshalText() w.ks=string(buf) returnerr } switchw.k.Kind() { casereflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: w.ks=strconv.FormatInt(w.k.Int(), 10) returnnilcasereflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: w.ks=strconv.FormatUint(w.k.Uint(), 10) returnnil } panic("unexpected map key type") // 正常情况不会走到这里, 如果走到了, 就有潜在bug}
方法(method)
- 方法的本质, 是将
receiver
作为函数的第一个参数:func (t *T) xxx(){}
->func xxx(t *T, ){}
- Go有一个语法糖, 无论使用
T
还是*T
的方法, 都可以调用, 但是需要注意细微的差别:
- 最重要的:
值类型 vs 指针类型
, 尤其是只能使用*T
的场景 - 实现 interface 时,
*T
可以使用所有方法, 而T
只能使用T
定义的方法
interface
- 最重要的一点:
interface = type + value
, 下面有个很好的例子
funcTestInterface(t*testing.T) { varaanyvarberrorvarc*errord :=&bt.Log(a==b) // truet.Log(a==c) // falset.Log(c==nil) // truet.Log(d==nil) // false}
使用 debug 查看:
- 尽量使用小接口(1-3个方法): 抽象程度更高 + 易于实现和测试 + 单一职责易于组合复用
- 接口 in, 接口体 out
Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)
- 尽量不要使用 any
Go 语言之父 Rob Pike 曾说过:空接口不提供任何信息(The empty interface says nothing)
写在最后
得益于Go的核心理念和设计哲学:
核心理念:简单、诗意、简洁(Simple, Poetic, Pithy)设计哲学:简单、显式、组合、并发和面向工程
Go 拥有编程语言中相对最少的关键字和类型系统, 让Go入门变得极易学习和上手, 希望这blog能帮助入门的gopher, 更快掌握Go, 同时避免一些易错点