?
? 作者 |?百度小程序團(tuán)隊(duì) ?
導(dǎo)讀?
introduction ? ?
本文收集一些使用Go開發(fā)過程中非常容易踩坑的case,所有的case都有具體的代碼示例,以及針對(duì)的代碼修復(fù)方法,以避免大家再次踩坑。通常這些坑的特點(diǎn)就是代碼正常能編譯,但運(yùn)行結(jié)果不及預(yù)期或是引入內(nèi)存漏洞的風(fēng)險(xiǎn)。 ? 全文7866字,預(yù)計(jì)閱讀時(shí)間20分鐘。
? GEEK TALK
01參數(shù)傳遞誤用? ??
1.1 誤對(duì)指針計(jì)算Sizeof
對(duì)任何指針進(jìn)行unsafe.Sizeof計(jì)算,返回的結(jié)果都是 8 (64位平臺(tái)下)。稍不注意就會(huì)引發(fā)錯(cuò)誤。
錯(cuò)誤示例:
?
func TestSizeofPtrBug(t *testing.T) { ????type?CodeLocation?struct?{ LineNo int64 ColNo int64 } cl := &CodeLocation{10, 20} size := unsafe.Sizeof(cl) fmt.Println(size) // always return 8 for point size }
?
建議使用示例:?jiǎn)为?dú)編寫一個(gè)只處理值大小的函數(shù) ValueSizeof。
?
func TestSizeofPtrWithoutBug(t *testing.T) {
type CodeLocation struct {
LineNo int64
ColNo int64
}
cl := &CodeLocation{10, 20}
size := ValueSizeof(cl)
fmt.Println(size) // 16
}
func ValueSizeof(v any) uintptr {
typ := reflect.TypeOf(v)
if typ.Kind() == reflect.Pointer {
return typ.Elem().Size()
}
return typ.Size()
}
? ?
?
1.2?可變參數(shù)為any類型時(shí),誤傳切片對(duì)象
當(dāng)參數(shù)的可變參數(shù)是any類型時(shí),傳入切片對(duì)象時(shí)一定要用展開方式。
?
appendAnyF := func(t []any, toAppend ...any) []any {
ret := append(t, toAppend...)
return ret
}
emptySlice := []any{}
slice2 := []any{"hello", "world"}
// bug append slice as a element
emptySlice = appendAnyF(emptySlice, slice2)
fmt.Println(emptySlice) // only 1 element [[hello world]]
emptySlice = []any{}
emptySlice = appendAnyF(emptySlice, slice2...)
fmt.Println(emptySlice) // [hello world]
? ?
?
1.3?數(shù)組是值傳遞
數(shù)組在函數(shù)或方法中入?yún)鬟f是值復(fù)制的方式,不能用入?yún)⒌姆绞竭M(jìn)函數(shù)或方法內(nèi)修改數(shù)組內(nèi)容進(jìn)行返回的。
示例代碼如下:
?
arr := [3]int{0, 1, 2}
f := func(v [3]int) {
v[0] = 100
}
f(arr) // no modify to arr
fmt.Println(arr) // [0 1 2]
? ?
?
1.4?切片擴(kuò)容后會(huì)新申請(qǐng)內(nèi)存,不再與內(nèi)存引用有任何關(guān)聯(lián)
這里坑在,如果從一個(gè)數(shù)組中引入一個(gè)切片,一旦這個(gè)切片引發(fā)擴(kuò)容后,則與原來的引用內(nèi)容沒有任何關(guān)系。
?
arr := []int{0, 1, 2}
f := func(v []int) {
v[0] = 100// can modify origin array
v = append(v, 4) // new memory allocated
v[0] = 50// no modify to origin array
}
f(arr)
fmt.Println(arr) // [100 1 2]
?
上面的示例代碼,擴(kuò)容切片前對(duì)內(nèi)容的修改可以影響到arr數(shù)組,說明是共享內(nèi)存地址引用的,一旦擴(kuò)容后,則是重新申請(qǐng)了內(nèi)存,與數(shù)組不再是一個(gè)內(nèi)存引用了。
1.5?返回參數(shù)盡量避免使用共享數(shù)據(jù)的切片對(duì)象,容易導(dǎo)致原始數(shù)據(jù)污染
這種場(chǎng)景就是如果通過函數(shù)返回值方式從一個(gè)大數(shù)組獲取部分內(nèi)部,盡量不要用切片共享的方式,可以使用copy的方式來替換。
下面的代碼,通過ReadUnsafe讀取切片后,修改內(nèi)容同步影響原始的內(nèi)容。
?
type Queue struct {
content []byte
pos int
}
func (q *Queue) ReadUnsafe(size int) []byte {
if q.pos+size >= len(q.content) {
return nil
}
pos := q.pos
q.pos = q.pos + size
return q.content[pos:q.pos]
}
func TestReadUnsafe(t *testing.T) {
c := [200]byte{}
q := &Queue{content: c[:]}
v := q.ReadUnsafe(10)
v[0] = 1
fmt.Println(q.content[0]) // 1 q.content值已經(jīng)被修改
}
?
正確的修改如下,使用copy創(chuàng)建一份新內(nèi)存:
?
func (q *Queue) ReadSafe(size int) []byte {
if q.pos+size >= len(q.content) {
return nil
}
pos := q.pos
q.pos = q.pos + size
ret := make([]byte, size)
copy(ret, q.content[pos:q.pos])
return ret
}
func TestReadSafe(t *testing.T) {
c := [200]byte{}
q := &Queue{content: c[:]}
v := q.ReadSafe(10)
v[0] = 1
fmt.Println(q.content[0]) // 0 q.content值安全
}
?
GEEK TALK
02指針相關(guān)使用的坑
2.1?誤保存uintptr值
uintptr保存的當(dāng)前地址的一個(gè)整型值,它一旦被獲取后,是不會(huì)被編譯器感知的,也就是它就是一個(gè)普通變量,不會(huì)追溯內(nèi)存真實(shí)地址變化。
?
slice := []int{0, 1, 2}
ptr := unsafe.Pointer(&slice[0]) // get array element:0 pointer
slice = append(slice, 3) // allocate new memory
ptr2 := unsafe.Pointer(&slice[0])
// ptr is 824633770392, ptr2 is 824633762896, ptr==ptr2 result is false
????fmt.Println(fmt.Sprintf("ptr?is?%d,?ptr2?is?%d,?ptr==ptr2?result?is?%v",?ptr,?ptr2,?ptr?==?ptr2))
? ?
?
2.2?len與cap 對(duì)空指針nil與空值返回相同
針對(duì)切片, 用len與cap操作時(shí),空值與nil都是返回0, 針對(duì)map, 用len操作時(shí),空值與nil都是返回0。
?
var slice []int = nil
fmt.Println(len(slice), cap(slice)) // 0 0
var slice2 []int = []int{}
fmt.Println(len(slice2), cap(slice2)) // 0 0
var mp map[int]int = nil
fmt.Println(len(mp)) // 0
var mp2 map[int]int = map[int]int{}
fmt.Println(len(mp2)) // 0
? ?
?
2.3?用new對(duì)map類型進(jìn)行初始化
用new對(duì)map進(jìn)行創(chuàng)建,編譯器不會(huì)報(bào)錯(cuò),但是無法對(duì)map進(jìn)行賦值操作的。正確應(yīng)使用make進(jìn)行內(nèi)存分配。
?
mp := new(map[int]int)
f := func(m map[int]int) {
m[10] = 10
}
f(*mp) // assignment to entry in nil map
? ?
?
2.4?空指針和空接口不等價(jià)
對(duì)于接口類型是可以用nil賦值的,但如果對(duì)于接口指針類型,其值對(duì)應(yīng)的并不一個(gè)空接口。Go語言編譯器似乎在這個(gè)處理,會(huì)特殊處理。
// MyErr just for demotype MyErr struct{}
func (e *MyErr) Error() string {
return""
}
func TestInterfacePointBug(t *testing.T) {
var e *MyErr = nil
var e2 error = e // e2 will never be nil.
fmt.Println(e2 == nil)
}
?
?
GEEK TALK
03函數(shù),方法與控制流相關(guān)
3.1 循環(huán)中使用閉包錯(cuò)誤引用同一個(gè)變量
原因分析:閉包捕獲外部變量,它不關(guān)心這些捕獲的變量或常量是否超出作用域,只要閉包在使用,這些變量就會(huì)一直存在。
?
type S struct {
A string
B string
C string
}
typ := reflect.TypeOf(S{})
funcArr := make([]func() string, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
f := func() string {
return typ.Field(i).Name
}
funcArr[i] = f
}
fmt.Println(funcArr[0]()) // error reflect: Field index out of bounds
?
所以上面的示例代碼,在循環(huán)中閉包函數(shù)只記錄了i變量的使用,當(dāng)循環(huán)結(jié)束后,i值變成了3。當(dāng)調(diào)用該匿名函數(shù)時(shí),就會(huì)引用i=3的值 ,出現(xiàn)越界的異常。
正確處理的方式如下,只需要閉包前處理一下把i變量賦值給一個(gè)新變量。
?
type S struct {
A string
B string
C string
}
typ := reflect.TypeOf(S{})
funcArr := make([]func() string, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
index := i // assign to a new variable
f := func() string {
name := typ.Field(index).Name
return name
}
funcArr[i] = f
}
fmt.Println(funcArr[0]()) // A
? ?
?
3.2?元素內(nèi)容較大時(shí),不要用range遍歷
用range來操作遍歷使用上非常方便,但是它的遍歷中是需要進(jìn)行值賦值操作,遇到元素占用的內(nèi)存比較大時(shí),性能就會(huì)影響較大。
下面是針對(duì)兩種方式做了一下基準(zhǔn)測(cè)試。
?
func CreateABigSlice(count int) [][4096]int {
ret := make([][4096]int, count)
for i := 0; i < count; i++ {
ret[i] = [4096]int{}
}
return ret
}
func BenchmarkRangeHiPerformance(b *testing.B) {
v := CreateABigSlice(1 << 12)
for i := 0; i < b.N; i++ {
len := len(v)
var tmp [4096]int
for k := 0; k < len; k++ {
tmp = v[k]
}
_ = tmp
}
}
func BenchmarkRangeLowPerformance(b *testing.B) {
v := CreateABigSlice(1 << 12)
for i := 0; i < b.N; i++ {
var tmp [4096]int
for _, e := range v {
tmp = e
}
_ = tmp
}
}
?
測(cè)試結(jié)果如下:range方式的性能較for方式相差了近10000倍。
?
cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz BenchmarkRangeHiPerformance-8 9767457 1255 ns/op BenchmarkRangeLowPerformance-8 975 11513216 ns/op PASS ok withoutbug/avoidtofix 26.270s? ?
?
3.3?循環(huán)內(nèi)調(diào)用defer造成銷毀處理延遲
在很多場(chǎng)景,在循環(huán)內(nèi)申請(qǐng)資源在循環(huán)完成后釋放,但是使用defer語句處理,是需要在當(dāng)前函數(shù)退出時(shí)才會(huì)執(zhí)行,在循環(huán)中是不會(huì)觸發(fā)的,導(dǎo)致資源延遲釋放。
?
func main() {
for i := 0; i < 5; i++ {
f, err := os.Open("./mygo.go")
if err != nil {
log.Fatal(err)
}
defer f.Close()
}
}
?
比較好的解決辦法就是在for循環(huán)里不要使用defer,直接進(jìn)行銷毀處理。
?
func main() {
for i := 0; i < 5; i++ {
f, err := os.Open("/path/to/file")
if err != nil {
log.Fatal(err)
}
f.Close()
}
}
? ?
?
3.4?Goroutine無法阻止主進(jìn)程退出
后臺(tái)Goroutine無法保證在方法退出來執(zhí)行完成。
?
func main() {
gofunc() {
time.Sleep(time.Second)
fmt.Println("run")
}()
}
? ?
?
3.5?Goroutine 拋panic會(huì)導(dǎo)致進(jìn)程退出
后臺(tái)Goroutine執(zhí)行中,如果拋panic并不進(jìn)行recover處理,會(huì)導(dǎo)致主進(jìn)程退出。
下面的代碼示例:
?
func main1() {
go func() {
panic("oh...")
}()
for i := 0; i < 3; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
fmt.Println("bye bye!")
}
?
修正代碼如下:
?
func main2() {
go func() {
defer func() {
recover() // should do some thing here
}()
panic("oh...")
}()
for i := 0; i < 3; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
fmt.Println("bye bye!")
}
? ?
?
3.6 recover函數(shù) 只在defer函數(shù)內(nèi)生效
需要注意:在非defer函數(shù)內(nèi),調(diào)用recover函數(shù),是不會(huì)有任何的執(zhí)行,也無法來處理panic錯(cuò)誤。
下面的示例代碼,是無法處理panic的錯(cuò)誤:
?
func NoTestDeferBug(t *testing.T) {
recover()
panic(1) // could not catch
}
func NoTestDeferBug2(t *testing.T) {
defer recover()
panic(1) // could not catch
}
?
正確的代碼如下:
?
func TestDeferFixed(t *testing.T) {
defer func() {
recover()
}()
panic("this is panic info") // could not catch
}
?
GEEK TALK
04并發(fā)與內(nèi)存同步相關(guān)
4.1?跨Goroutine之間不支持順序一致性內(nèi)存模型
在Go語言的內(nèi)存模型設(shè)計(jì)中, 內(nèi)存寫入順序性只能保障在單一Goroutine內(nèi)一致,跨Goroutine之間無法保障監(jiān)測(cè)變量操作順序的一致性。
下面是官方的例子:
?
package main
var msg string
var done bool
func setup() {
msg = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
println(msg)
}
?
上面代碼的問題是,不能保證在 main 中對(duì) done 的寫入的監(jiān)測(cè)時(shí), 會(huì)對(duì)變量a的寫入也進(jìn)行監(jiān)測(cè),因此該程序也可能會(huì)打印出一個(gè)空字符串。更糟的是,由于在兩個(gè)線程之間沒有同步事件,因此無法保證對(duì) done 的寫入總能被 main 監(jiān)測(cè)到。main 中的循環(huán)不保證一定能結(jié)束。
解決辦法就是使用顯示同步方案, 使用通道進(jìn)行同步通信。
?
package main
var msg string
var done = make(chan bool)
func setup() {
msg = "hello, world"
done <- true
}
func main() {
go setup()
<-done
println(msg)
}
?
這樣就可以保證代碼執(zhí)行過程中必定輸出 hello,world。
更多內(nèi)存同步閱讀材料:https://go-zh.org/ref/mem
GEEK TALK
05序列化相關(guān)
5.1?基于指針參數(shù)方式傳遞的反序列功能,都不會(huì)初始化要反序列化的對(duì)象字段
該問題經(jīng)常發(fā)生的原因是基于指針參數(shù)方式傳遞的反序列函數(shù)其實(shí)做的只是值覆蓋的功能,并不會(huì)把要反序化的對(duì)象的所有值進(jìn)行初始化操作,這樣就會(huì)導(dǎo)致未覆蓋的值的保留. 像 json.Unmarshal, xml.Unmarshal 函數(shù)等。
下面是基于json對(duì)map 類型的變量進(jìn)行json.Unmarshal的問題示例:
?
package main
import (
"encoding/json"
"fmt"
)
func main() {
val := map[string]int{}
s1 := `{"k1":1, "k2":2, "k3":3}`
s2 := `{"k1":11, "k2":22, "k4":44}`
json.Unmarshal([]byte(s1), &val)
fmt.Println(s1, val)
json.Unmarshal([]byte(s2), &val)
fmt.Println(s2, val)
}
?
輸出:
?
{"k1":1, "k2":2, "k3":3} map[k1:1 k2:2 k3:3]
{"k1":11,?"k2":22,?"k4":44}?map[k1:11?k2:22?k3:3?k4:44]
?
由于 json.UnMarshal 方法只會(huì)新增和覆蓋 map 中的 key,不會(huì)刪除 key。雖然第二個(gè)json字符串中沒有k3的內(nèi)容,但輸出結(jié)果中依然保留在了k3的內(nèi)容。
要解決這個(gè)問題,每次 unmarshal 之前都重新聲明變量即可。
GEEK TALK
06其它雜項(xiàng)
6.1?數(shù)字類型轉(zhuǎn)換越界陷阱
Go語言中,任何操作符不會(huì)改變變量類型,下面示例引入一個(gè)坑, 出現(xiàn)位移越界。
?
func TestOverFlowBug(t *testing.T) {
var num int16 = 5000
var result int64 = int64(num << 9)
fmt.Println(result) // 4096 overflow
}
?
修正方式如下,需要操作前對(duì)類型轉(zhuǎn)換:
?
func TestOverFlowFixed(t *testing.T) {
var num int16 = 5000
var result int64 = int64(num) << 9
fmt.Println(result) // 2560000
}
? ?
?
6.2?map遍歷是順序不固定
map的實(shí)現(xiàn)是通hash表進(jìn)行分桶定位,同時(shí)map的遍歷引入了隨機(jī)實(shí)現(xiàn),所以每次遍歷的順序都可能變化。
?
mp := map[int]int{}
for i := 0; i < 20; i++ {
mp[i] = i
}
for k, v := range mp {
fmt.Println(k, v)
}
?
審核編輯:湯梓紅
電子發(fā)燒友App















評(píng)論