最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

我為什么放棄Go語言?

2023-06-24 14:20 作者:你認(rèn)識張大衛(wèi)嗎  | 我要投稿

你在什么時候會產(chǎn)生“想要放棄用 Go 語言”的念頭?也許是在用 Go 開發(fā)過程中,接連不斷踩坑的時候。本文作者提煉和總結(jié)《100 Go Mistakes and How to Avoid Them》里的精華內(nèi)容,并結(jié)合自身的工作經(jīng)驗,盤點了 Go 的常見典型錯誤,撰寫了這篇超全避坑指南。讓我們跟隨文章,一起重拾用 Go 的信心~

目錄

1 注意 shadow 變量

2 慎用 init 函數(shù)

3 embed types 優(yōu)缺點

4 Functional Options Pattern 傳遞參數(shù)

5 小心八進制整數(shù)

6 float 的精度問題

7 slice 相關(guān)注意點 slice 相關(guān)注意點

8 注意 range

9 注意 break 作用域

10 defer

11 string 相關(guān)

12 interface 類型返回的非 nil 問題

13 Error

14 happens before 保證

15 Context Values

16 應(yīng)多關(guān)注 goroutine 何時停止

17 Channel

18 string format 帶來的 dead lock

19 錯誤使用 sync.WaitGroup

20 不要拷貝 sync 類型

21 time.After 內(nèi)存泄露

22 HTTP body 忘記 Close 導(dǎo)致的泄露

23 Cache line

24 關(guān)于 False Sharing 造成的性能問題

25 內(nèi)存對齊

26 逃逸分析

27 byte slice 和 string 的轉(zhuǎn)換優(yōu)化

28 容器中的 GOMAXPROCS

29 總結(jié)

01、注意 shadow 變量

go復(fù)制代碼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) ?}

在上面這段代碼中,聲明了一個 client 變量,然后使用 tracing 控制變量的初始化,可能是因為沒有聲明 err 的緣故,使用的是 := 進行初始化,那么會導(dǎo)致外層的 client 變量永遠(yuǎn)是 nil。這個例子實際上是很容易發(fā)生在我們實際的開發(fā)中,尤其需要注意。

如果是因為 err 沒有初始化的緣故,我們在初始化的時候可以這么做:

go復(fù)制代碼var client *http.Client ??var err error ??if tracing { ?? ?client, err = createClientWithTracing() ? ?} else { ?? ?... ??} ?? ?if err != nil { // 防止重復(fù)代碼 ?? ? ? ?return err ? ?}

或者內(nèi)層的變量聲明換一個變量名字,這樣就不容易出錯了。

我們也可以使用工具分析代碼是否有 shadow,先安裝一下工具:

go復(fù)制代碼go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow

然后使用 shadow 命令:

go復(fù)制代碼go vet -vettool=C:\Users\luozhiyun\go\bin\shadow.exe .\main.go # command-line-arguments .\main.go:15:3: declaration of "client" shadows declaration at line 13 .\main.go:21:3: declaration of "client" shadows declaration at line 13

02、慎用 init 函數(shù)

使用 init 函數(shù)之前需要注意下面幾件事:

2.1 init 函數(shù)會在全局變量之后被執(zhí)行

init 函數(shù)并不是最先被執(zhí)行的,如果聲明了 const 或全局變量,那么 init 函數(shù)會在它們之后執(zhí)行:

go復(fù)制代碼package main ?import "fmt" ?var a = func() int { ??fmt.Println("a") ??return 0 }() ?func init() { ??fmt.Println("init") } ?func main() { ??fmt.Println("main") } ?// output a initmain

2.2 init 初始化按解析的依賴關(guān)系順序執(zhí)行

比如 main 包里面有 init 函數(shù),依賴了 redis 包,main 函數(shù)執(zhí)行了 redis 包的 Store 函數(shù),恰好 redis 包里面也有 init 函數(shù),那么執(zhí)行順序會是:

還有一種情況,如果是使用 "import _ foo" 這種方式引入的,也是會先調(diào)用 foo 包中的 init 函數(shù)。

2.3 擾亂單元測試

比如我們在 init 函數(shù)中初始了一個全局的變量,但是單測中并不需要,那么實際上會增加單測得復(fù)雜度,比如:

go復(fù)制代碼var db *sql.DB func init(){ ??dataSourceName := os.Getenv("MYSQL_DATA_SOURCE_NAME") ?? ?d, err := sql.Open("mysql", dataSourceName) ?? ?if err != nil { ?? ? ? ?log.Panic(err) ?? ?} ?? ?db = d}

在上面這個例子中 init 函數(shù)初始化了一個 db 全局變量,那么在單測的時候也會初始化一個這樣的變量,但是很多單測其實是很簡單的,并不需要依賴這個東西。

03、embed types 優(yōu)缺點

embed types 指的是我們在 struct 里面定義的匿名的字段,如:

go復(fù)制代碼type Foo struct { ??Bar } type Bar struct { ??Baz int}

那么在上面這個例子中,我們可以通過 Foo.Baz 直接訪問到成員變量,當(dāng)然也可以通過 Foo.Bar.Baz 訪問。

這樣在很多時候可以增加我們使用的便捷性,如果沒有使用 embed types 那么可能需要很多代碼,如下:

go復(fù)制代碼type Logger struct { ?? ? ? ?writeCloser io.WriteCloser } ?func (l Logger) Write(p []byte) (int, error) { ?? ? ? ?return l.writeCloser.Write(p) } ?func (l Logger) Close() error { ?? ? ? ?return l.writeCloser.Close() } ?func main() { ?? ? ? ?l := Logger{writeCloser: os.Stdout} ?? ? ? ?_, _ = l.Write([]byte("foo")) ?? ? ? ?_ = l.Close()}

如果使用了?embed types 我們的代碼可以變得很簡潔

go復(fù)制代碼type Logger struct { ?? ? ? ?io.WriteCloser } ?func main() { ?? ? ? ?l := Logger{WriteCloser: os.Stdout} ?? ? ? ?_, _ = l.Write([]byte("foo")) ?? ? ? ?_ = l.Close()}

但是同樣它也有缺點,有些字段我們并不想 export ,但是 embed types 可能給我們帶出去,例如:

go復(fù)制代碼type InMem struct { ??sync.Mutex ??m map[string]int } ?func New() *InMem { ?? return &InMem{m: make(map[string]int)}}

Mutex 一般并不想 export, 只想在 InMem 自己的函數(shù)中使用,如:

go復(fù)制代碼func (i *InMem) Get(key string) (int, bool) { ??i.Lock() ??v, contains := i.m[key] ??i.Unlock() ??return v, contains}

但是這么寫卻可以讓拿到 InMem 類型的變量都可以使用它里面的 Lock 方法:

css復(fù)制代碼m := inmem.New() m.Lock() // ??

04、Functional Options Pattern傳遞參數(shù)

這種方法在很多 Go 開源庫都有看到過使用,比如 zap、GRPC 等。

它經(jīng)常用在需要傳遞和初始化校驗參數(shù)列表的時候使用,比如我們現(xiàn)在需要初始化一個 HTTP server,里面可能包含了 port、timeout 等等信息,但是參數(shù)列表很多,不能直接寫在函數(shù)上,并且我們要滿足靈活配置的要求,畢竟不是每個 server 都需要很多參數(shù)。那么我們可以:

設(shè)置一個不導(dǎo)出的 struct 叫 options,用來存放配置參數(shù); 創(chuàng)建一個類型 type Option func(options *options) error,用這個類型來作為返回值;

比如我們現(xiàn)在要給 HTTP server 里面設(shè)置一個 port 參數(shù),那么我們可以這么聲明一個 WithPort 函數(shù),返回 Option 類型的閉包,當(dāng)這個閉包執(zhí)行的時候會將 options 的 port 填充進去:

go復(fù)制代碼type options struct { ?? ? ? ?port *int } ?type Option func(options *options) error ?func WithPort(port int) Option { ?? ? ? ? // 所有的類型校驗,賦值,初始化啥的都可以放到這個閉包里面做 ?? ? ? ?return func(options *options) error { ?? ? ? ? ? ? ? ?if port < 0 { ?? ? ? ? ? ? ? ? ? ? ? ?return errors.New("port should be positive") ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?options.port = &port ?? ? ? ? ? ? ? ?return nil ?? ? ? ?}}

假如我們現(xiàn)在有一個這樣的 Option 函數(shù)集,除了上面的 port 以外,還可以填充 timeout 等。然后我們可以利用 NewServer 創(chuàng)建我們的 server:

go復(fù)制代碼func NewServer(addr string, opts ...Option) (*http.Server, error) { ?? ? ? ?var options options ?? ? ? ?// 遍歷所有的 Option ?? ? ? ?for _, opt := range opts { ?? ? ? ? ? ? ? ?// 執(zhí)行閉包 ?? ? ? ? ? ? ? ?err := opt(&options) ?? ? ? ? ? ? ? ?if err != nil { ?? ? ? ? ? ? ? ? ? ? ? ?return nil, err ?? ? ? ? ? ? ? ?} ?? ? ? ?} ? ? ? ? ?// 接下來可以填充我們的業(yè)務(wù)邏輯,比如這里設(shè)置默認(rèn)的port 等等 ?? ? ? ?var port int ?? ? ? ?if options.port == nil { ?? ? ? ? ? ? ? ?port = defaultHTTPPort ?? ? ? ?} else { ?? ? ? ? ? ? ? ?if *options.port == 0 { ?? ? ? ? ? ? ? ? ? ? ? ?port = randomPort() ?? ? ? ? ? ? ? ?} else { ?? ? ? ? ? ? ? ? ? ? ? ?port = *options.port ?? ? ? ? ? ? ? ?} ?? ? ? ?} ? ? ? ? ?// ...}

初始化 server:

css復(fù)制代碼server, err := httplib.NewServer("localhost", ? ? ? ? ? ? ? ? httplib.WithPort(8080), ? ? ? ? ? ? ? ?httplib.WithTimeout(time.Second))

這樣寫的話就比較靈活,如果只想生成一個簡單的 server,我們的代碼可以變得很簡單:

css復(fù)制代碼server, err := httplib.NewServer("localhost")

05、小心八進制整數(shù)

比如下面例子:

bash復(fù)制代碼sum := 100 + 010 ?fmt.Println(sum)

你以為要輸出110,其實輸出的是 108,因為在 Go 中以 0 開頭的整數(shù)表示八進制。

它經(jīng)常用在處理 Linux 權(quán)限相關(guān)的代碼上,如下面打開一個文件:

lua復(fù)制代碼file, err := os.OpenFile("foo", os.O_RDONLY, 0644)

所以為了可讀性,我們在用八進制的時候最好使用 "0o" 的方式表示,比如上面這段代碼可以表示為:

lua復(fù)制代碼file, err := os.OpenFile("foo", os.O_RDONLY, 0o644)

06、float 的精度問題

在 Go 中浮點數(shù)表示方式和其他語言一樣,都是通過科學(xué)計數(shù)法表示,float 在存儲中分為三部分:

符號位(Sign): 0代表正,1代表為負(fù) 指數(shù)位(Exponent):用于存儲科學(xué)計數(shù)法中的指數(shù)數(shù)據(jù),并且采用移位存儲 尾數(shù)部分(Mantissa):尾數(shù)部分

計算規(guī)則我就不在這里展示了,感興趣的可以自己去查查,我這里說說這種計數(shù)法在 Go 里面會有哪些問題。

go復(fù)制代碼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.}

在上面這段代碼中,我們簡單地做了一下加法:

nExact resultf1f21010010.00110010.00110010.0011k11000.111000.111000.11m1.01E+061.01E+061.01E+06

可以看到 n 越大,誤差就越大,并且 f2 的誤差是小于 f1的。

對于乘法我們可以做下面的實驗:

css復(fù)制代碼a := 100000.001 b := 1.0001 c := 1.0002 ?fmt.Println(a * (b + c))fmt.Println(a*b + a*c)

輸出:

復(fù)制代碼200030.00200030004 200030.0020003

正確輸出應(yīng)該是 200030.0020003,所以它們實際上都有一定的誤差,但是可以看到先乘再加精度丟失會更小。

如果想要準(zhǔn)確計算浮點的話,可以嘗試 "github.com/shopspring/…" 庫,換成這個庫我們再來計算一下:

css復(fù)制代碼a := decimal.NewFromFloat(100000.001) b := decimal.NewFromFloat(1.0001) c := decimal.NewFromFloat(1.0002) ?fmt.Println(a.Mul(b.Add(c))) //200030.0020003

07、slice 相關(guān)注意點

7.1 區(qū)分 slice 的 length 和 capacity

首先讓我們初始化一個帶有 length 和 capacity 的 slice :

go復(fù)制代碼s := make([]int, 3, 6)

在 make 函數(shù)里面,capacity 是可選的參數(shù)。上面這段代碼我們創(chuàng)建了一個 length 是 3,capacity 是 6 的 slice,那么底層的數(shù)據(jù)結(jié)構(gòu)是這樣的:

slice 的底層實際上指向了一個數(shù)組。當(dāng)然,由于我們的 length 是 3,所以這樣設(shè)置 s[4] = 0 會 panic 的。需要使用 append 才能添加新元素。

go復(fù)制代碼panic: runtime error: index out of range [4] with length 3

當(dāng) appned 超過 cap 大小的時候,slice 會自動幫我們擴容,在元素數(shù)量小于 1024 的時候每次會擴大一倍,當(dāng)超過了 1024 個元素每次擴大 25% 。

有時候我們會使用 :操作符從另一個 slice 上面創(chuàng)建一個新切片:

go復(fù)制代碼s1 := make([]int, 3, 6) s2 := s1[1:3]

實際上這兩個 slice 還是指向了底層同樣的數(shù)組,構(gòu)如下:

由于指向了同一個數(shù)組,那么當(dāng)我們改變第一個槽位的時候,比如 s1[1]=2,實際上兩個 slice 的數(shù)據(jù)都會發(fā)生改變:

但是當(dāng)我們使用 append 的時候情況會有所不同:

scss復(fù)制代碼s2 = append(s2, 3) ?fmt.Println(s1) // [0 2 0] fmt.Println(s2) // [2 0 3]

s1 的 len 并沒有被改變,所以看到的還是3元素。

還有一件比較有趣的細(xì)節(jié)是,如果再接著 append s1 那么第四個元素會被覆蓋掉:

scss復(fù)制代碼s1 = append(s1, 4) ??fmt.Println(s1) // [0 2 0 4] ??fmt.Println(s2) // [2 0 4]

我們再繼續(xù) append s2 直到 s2 發(fā)生擴容,這個時候會發(fā)現(xiàn) s2 實際上和 s1 指向的不是同一個數(shù)組了:

scss復(fù)制代碼s2 = append(s2, 5, 6, 7) fmt.Println(s1) //[0 2 0 4] fmt.Println(s2) //[2 0 4 5 6 7]

除了上面這種情況,還有一種情況 append 會產(chǎn)生意想不到的效果:

go復(fù)制代碼s1 := []int{1, 2, 3} s2 := s1[1:2] s3 := append(s2, 10)

如果 print 它們應(yīng)該是這樣:

css復(fù)制代碼s1=[1 2 10], s2=[2], s3=[2 10]

7.2 slice 初始化

對于 slice 的初始化實際上有很多種方式:

scss復(fù)制代碼func main() { ?? ? ? ?var s []string ?? ? ? ?log(1, s) ? ? ? ? ?s = []string(nil) ?? ? ? ?log(2, s) ? ? ? ? ?s = []string{} ?? ? ? ?log(3, s) ? ? ? ? ?s = make([]string, 0) ?? ? ? ?log(4, s) } ?func log(i int, s []string) { ?? ? ? ?fmt.Printf("%d: empty=%t\tnil=%t\n", i, len(s) == 0, s == nil)}

輸出:

ini復(fù)制代碼1: empty=true ? nil=true 2: empty=true ? nil=true 3: empty=true ? nil=false 4: empty=true ? nil=false

前兩種方式會創(chuàng)建一個 nil 的 slice,后兩種會進行初始化,并且這些 slice 的大小都為 0 。

對于 var s []string 這種方式來說,好處就是不用做任何的內(nèi)存分配。比如下面場景可能可以節(jié)省一次內(nèi)存分配:

csharp復(fù)制代碼func f() []string { ?? ? ? ?var s []string ?? ? ? ?if foo() { ?? ? ? ? ? ? ? ?s = append(s, "foo") ?? ? ? ?} ?? ? ? ?if bar() { ?? ? ? ? ? ? ? ?s = append(s, "bar") ?? ? ? ?} ?? ? ? ?return s}

對于 s := []string{} 這種方式來說,它比較適合初始化一個已知元素的 slice

go復(fù)制代碼s := []string{"foo", "bar", "baz"}

如果沒有這個需求其實用 var s []string 比較好,反正在使用的適合都是通過 append 添加元素, var s []string 還能節(jié)省一次內(nèi)存分配。

如果我們初始化了一個空的 slice, 那么最好是使用 len(xxx) == 0來判斷 slice 是不是空的,如果使用 nil 來判斷可能會永遠(yuǎn)非空的情況,因為對于 s := []string{} 和 s = make([]string, 0) 這兩種初始化都是非 nil 的。

對于 []string(nil) 這種初始化的方式,使用場景很少,一種比較方便地使用場景是用它來進行 slice 的 copy:

go復(fù)制代碼src := []int{0, 1, 2} dst := append([]int(nil), src...)

對于 make 來說,它可以初始化 slice 的 length 和 capacity,如果我們能確定 slice 里面會存放多少元素,從性能的角度考慮最好使用 make 初始化好,因為對于一個空的 slice append 元素進去每次達(dá)到閾值都需要進行擴容,下面是填充 100 萬元素的 benchmark:

bash復(fù)制代碼BenchmarkConvert_EmptySlice-4 22 49739882 ns/op BenchmarkConvert_GivenCapacity-4 86 13438544 ns/op BenchmarkConvert_GivenLength-4 91 12800411 ns/op

可以看到,如果我們提前填充好 slice 的容量大小,性能是空 slice 的四倍,因為少了擴容時元素復(fù)制以及重新申請新數(shù)組的開銷。

7.3 copy slice

css復(fù)制代碼src := []int{0, 1, 2} var dst []int copy(dst, src) fmt.Println(dst) // []

使用 copy 函數(shù) copy slice 的時候需要注意,上面這種情況實際上會 copy 失敗,因為對 slice 來說是由 length 來控制可用數(shù)據(jù),copy 并沒有復(fù)制這個字段,要想 copy 我們可以這么做:

go復(fù)制代碼src := []int{0, 1, 2} dst := make([]int, len(src)) copy(dst, src) fmt.Println(dst) //[0 1 2]

除此之外也可以用上面提到的:

go復(fù)制代碼src := []int{0, 1, 2} dst := append([]int(nil), src...)

7.4 slice capacity內(nèi)存釋放問題

先來看個例子:

css復(fù)制代碼type Foo struct { ??v []byte } ?func keepFirstTwoElementsOnly(foos []Foo) []Foo { ??return foos[:2] } ?func main() { ??foos := make([]Foo, 1_000) ??printAlloc() ? ?for i := 0; i < len(foos); i++ { ?? ?foos[i] = Foo{ ?? ? ?v: make([]byte, 1024*1024), ?? ?} ??} ??printAlloc() ? ?two := keepFirstTwoElementsOnly(foos) ??runtime.GC() ??printAlloc() ??runtime.KeepAlive(two)}

上面這個例子中使用 printAlloc 函數(shù)來打印內(nèi)存占用:

swift復(fù)制代碼func printAlloc() { ??var m runtime.MemStats ??runtime.ReadMemStats(&m) ??fmt.Printf("%d KB\n", m.Alloc/1024)}

上面 foos 初始化了 1000 個容量的 slice ,里面 Foo struct 每個都持有 1M 內(nèi)存的 slice,然后通過 keepFirstTwoElementsOnly 返回持有前兩個元素的 Foo 切片,我們的想法是手動執(zhí)行 GC 之后其他的 998 個 Foo 會被 GC 銷毀,但是輸出結(jié)果如下:

復(fù)制代碼387 KB 1024315 KB1024319 KB

實際上并沒有,原因就是實際上 keepFirstTwoElementsOnly 返回的 slice 底層持有的數(shù)組是和 foos 持有的同一個:

所以我們真的要只返回 slice 的前2個元素的話應(yīng)該這樣做:

go復(fù)制代碼func keepFirstTwoElementsOnly(foos []Foo) []Foo { ?? ? ? ?res := make([]Foo, 2) ?? ? ? ?copy(res, foos) ?? ? ? ?return res}

不過上面這種方法會初始化一個新的 slice,然后將兩個元素 copy 過去。不想進行多余的分配可以這么做:

css復(fù)制代碼func keepFirstTwoElementsOnly(foos []Foo) []Foo { ?? ? ? ?for i := 2; i < len(foos); i++ { ?? ? ? ? ? ? ? ?foos[i].v = nil ?? ? ? ?} ?? ? ? ?return foos[:2]}

我為什么放棄Go語言?的評論 (共 條)

分享到微博請遵守國家法律
盈江县| 成安县| 彭水| 金秀| 广灵县| 鄂尔多斯市| 博湖县| 毕节市| 龙州县| 马龙县| 舟曲县| 肃南| 高邮市| 贡山| 定西市| 利辛县| 尼勒克县| 卫辉市| 铜梁县| 墨玉县| 巫溪县| 枣强县| 隆子县| 淮滨县| 昂仁县| 玛纳斯县| 浪卡子县| 策勒县| 天全县| 嘉黎县| 镇平县| 望江县| 大庆市| 重庆市| 宁安市| 托克托县| 大渡口区| 宁远县| 湖北省| 湾仔区| 芜湖县|