咪免直播高品质美女在线视频互动社区_咪免直播官方版_咪免直播直播视频在线观看免费版下载

您的位置:首頁 > 軟件教程 > 教程 > Go語言的100個(gè)錯(cuò)誤使用場景(61-68)|并發(fā)實(shí)踐

Go語言的100個(gè)錯(cuò)誤使用場景(61-68)|并發(fā)實(shí)踐

來源:好特整理 | 時(shí)間:2024-04-13 15:34:52 | 閱讀:150 |  標(biāo)簽: 錯(cuò)誤 GO   | 分享到:

我的愿景是以這套文章,在保持權(quán)威性的基礎(chǔ)上,脫離對原文的依賴,對這100個(gè)場景進(jìn)行篇幅合適的中文講解。所涉內(nèi)容較多,總計(jì)約 8w 字,這是該系列的第八篇文章,對應(yīng)書中第61-68個(gè)錯(cuò)誤場景。

前言

大家好,這里是白澤。 《Go語言的100個(gè)錯(cuò)誤以及如何避免》 是最近朋友推薦我閱讀的書籍,我初步瀏覽之后,大為驚喜。就像這書中第一章的標(biāo)題說到的:“ Go: Simple to learn but hard to master ”,整本書通過分析100個(gè)錯(cuò)誤使用 Go 語言的場景,帶你深入理解 Go 語言。

我的愿景是以這套文章,在保持權(quán)威性的基礎(chǔ)上,脫離對原文的依賴,對這100個(gè)場景進(jìn)行篇幅合適的中文講解。所涉內(nèi)容較多,總計(jì)約 8w 字,這是該系列的第八篇文章,對應(yīng)書中第61-68個(gè)錯(cuò)誤場景。

? 當(dāng)然,如果您是一位 Go 學(xué)習(xí)的新手,您可以在我開源的 學(xué)習(xí)倉庫 中,找到針對 《Go 程序設(shè)計(jì)語言》 英文書籍的配套筆記,其他所有文章也會整理收集在其中。

? B站: 白澤talk ,公眾號【白澤talk】,聊天交流群:622383022,原書電子版可以加群獲取。

前文鏈接:

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(1-10)|代碼和項(xiàng)目組織》

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(11-20)|項(xiàng)目組織和數(shù)據(jù)類型》

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(21-29)|數(shù)據(jù)類型》

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(30-40)|數(shù)據(jù)類型與字符串使用》

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(40-47)|字符串&函數(shù)&方法》

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(48-54)|錯(cuò)誤管理》

  • 《Go語言的100個(gè)錯(cuò)誤使用場景(55-60)|并發(fā)基礎(chǔ)》

9. 并發(fā)實(shí)踐

? 章節(jié)概述

  • 防止發(fā)生 goroutine 和 channel 中的常見錯(cuò)誤
  • 理解標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)在并發(fā)場景的使用
  • 使用標(biāo)準(zhǔn)庫和一些擴(kuò)展
  • 避免數(shù)據(jù)競爭和死鎖

9.1 context 的不恰當(dāng)傳播(#61)

context 作為承載上下文的實(shí)例,經(jīng)常在各個(gè)函數(shù)之間傳播,由于 context.Context 本身是一個(gè)接口,它聲明了四個(gè)方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

當(dāng)一個(gè) context 因?yàn)檫^期或者被手動 cancel,都會導(dǎo)致上下文關(guān)閉。此時(shí)可以從 Done() 獲得的 channel 中獲得關(guān)閉信號,以及從 Err() 方法獲得原因。

這也導(dǎo)致了,在傳遞 context 實(shí)例的時(shí)候,因?yàn)橐恍┰驅(qū)е聜鬟f給子步驟的 context 已經(jīng)關(guān)閉,但是子步驟中需要使用到,從而造成混淆。

? 假設(shè)有一個(gè)場景,針對收到的一個(gè) HTTP 請求,服務(wù)端會處理一些任務(wù),得到結(jié)果A,同時(shí)將處理結(jié)果A通過 Kafka 異步發(fā)送一個(gè)事件,同時(shí)主協(xié)程返回任務(wù)處理結(jié)果A給客戶端。

func handler(w http.ResponseWriter, r *http.Request) {
    response, err := doSomeTask(r.Context(), r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    go func() {
        err := publish(r.Context(), response)
        // Do something with err
    }()
    
    writeResponse(response)
}

考慮以下三個(gè)場景:

  1. 客戶端請求關(guān)閉
  2. 如果是 HTTP/2 的請求,當(dāng)請求被取消
  3. 當(dāng) response 已經(jīng)被返回給客戶端

前兩個(gè)場景,如果在執(zhí)行完 doSomeTask() 的到 response 并調(diào)用 publish 后,請求被取消,則 publish 函數(shù)是可以允許接收一個(gè)被關(guān)閉的 context 實(shí)例的,只要在函數(shù)內(nèi)判斷當(dāng) context 被取消時(shí),不發(fā)送消息即可。 (當(dāng)然不做任何處理,允許發(fā)送也是沒有問題的)

但如果是已經(jīng)將 writeResponse(response) 觸發(fā),響應(yīng)給客戶端,則 *http.Request 關(guān)聯(lián)的 context 會被取消,此時(shí)如果在 publish() 函數(shù)中,做了 context 實(shí)例是否被取消的判斷,則會出現(xiàn)混淆。因?yàn)榇藭r(shí)是執(zhí)行成功的鏈路,只是 go func() 執(zhí)行邏輯因?yàn)楫惒降脑蚵,kafka 消息還是需要發(fā)送的。

? 解決方案:

type detach struct {
    ctx context.Context
}

func (d detach) Deadline() (time.Time, bool) {
    return time.Time{}, false
}

func (d detach) Done() <-chan struct{} {
    return nil
}

func (d detach) Err() error {
    return nil
}

func (d detach) Value(key any) any {
    return d.ctx.Value(key)
}
------------------------
// 使用方式
err := publish(detach{ctx: r.Context()}, response)

自定義 context 實(shí)例,將 Done() 和 Err() 方法失效,當(dāng)不希望 context 的關(guān)閉對子步驟造成影響,可以通過這種方式,保留從原 context.Context 的實(shí)例中,獲取上下文參數(shù) value 的能力。

9.2 開啟一個(gè)協(xié)程但不知道何時(shí)關(guān)閉(#62)

goroutine 泄漏:

協(xié)程啟動將占用一個(gè)約 2KB 大小的棧內(nèi)存空間,并隨著使用增長或者收縮占用的空間,一個(gè)協(xié)程可以持有一個(gè)引用類型的變量,且分配在堆上。goroutine 也可以持有 HTTP 鏈接、數(shù)據(jù)庫連接池等各種資源,如果協(xié)程發(fā)生了泄漏,則這些協(xié)程內(nèi)原本應(yīng)該被優(yōu)雅釋放的資源也將發(fā)生泄漏。

? 錯(cuò)誤示例一:

ch := foo()
go func() {
    for v := range ch {
        //..
    }  
}()

在上述示例中,新創(chuàng)建的協(xié)程只有當(dāng)主協(xié)程創(chuàng)建的 channel 被關(guān)閉的時(shí)候才會結(jié)束,但是如果外部沒有主動關(guān)閉,則這個(gè)子協(xié)程會發(fā)生泄漏,永遠(yuǎn)無法關(guān)閉。

? 錯(cuò)誤示例二:

假設(shè)應(yīng)用執(zhí)行之前需要通過一個(gè)函數(shù)去監(jiān)聽外部的配置信息。

func main() {
    newWatcher()
    
    // Run the application
}

type watcher struct{ /* Some resource */}

func newWatcher() {
    w := watcher{}
    go w.watch()
}

上述代碼的問題在于,newWatcher 函數(shù)內(nèi)啟動的子協(xié)程會由于主協(xié)程的結(jié)束而被迫終止,導(dǎo)致 watcher 結(jié)構(gòu)體所持有的資源,沒有被優(yōu)雅關(guān)閉。

? 錯(cuò)誤示例三:

在錯(cuò)誤示例二的基礎(chǔ)上,容易犯的一個(gè)錯(cuò)誤是,認(rèn)為可以通過傳遞一個(gè) context 來感知主協(xié)程關(guān)閉,從而控制子協(xié)程資源的釋放。

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    newWatcher(ctx)
    // Run the application
}

type watcher struct{ /* Some resource */}

func newWatcher(ctx context.Context) {
    w := watcher{}
    go w.watch(ctx)
}

錯(cuò)誤原因:此時(shí)主協(xié)程如果關(guān)閉了傳遞給 watcher 結(jié)構(gòu)體的 context,但是依舊有可能主函數(shù)直接執(zhí)行完成,關(guān)閉了,子協(xié)程即使收到了 context 關(guān)閉的信號,依舊不一定有時(shí)間完成資源的釋放。

? 正確示例:

func main() {
    w := newWatcher()
    defer w.close()
    
    // Run the application
}

func newWatcher() watcher {
    w := watcher{}
    go w.watch()
    return w
}

type watcher struct{ /* Some resource */}

func (w watcher) close() {
    // Close the resources
}

前幾個(gè)示例出現(xiàn)資源釋放問題的原因在于,在父協(xié)程關(guān)閉的時(shí)候,并沒有阻塞等待子協(xié)程資源的釋放,因此正確示例中,主協(xié)程在 return 之前,主動關(guān)閉 watcher 結(jié)構(gòu)體持有的資源,實(shí)現(xiàn)優(yōu)雅退出。

? 最佳實(shí)踐:

將 goroutine 當(dāng)作一種資源,在創(chuàng)建的開始就需要考慮何時(shí)關(guān)閉,并且如果 goroutine 持有了其他的資源,則需要一并考慮這些資源的釋放。

如果要關(guān)閉主協(xié)程,務(wù)必將所有的釋放工作,提前完成。

9.3 在循環(huán)中沒有謹(jǐn)慎使用協(xié)程(#63)

錯(cuò)誤示例:

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

for _, i := range s {
    go func() {
        fmt.Println(i)  
    }()
}

// 輸出結(jié)果可能是:233,333

循環(huán)結(jié)構(gòu)內(nèi)部的 goroutine,這種閉包的寫法,持有的 i 是同一個(gè)變量,因此雖然 i 是按照順序1,2,3賦值的,但是并不能決定協(xié)程是在 i 等于幾的時(shí)候觸發(fā)打印操作。

比如出現(xiàn)233的執(zhí)行順序圖示如下:

Go語言的100個(gè)錯(cuò)誤使用場景(61-68)|并發(fā)實(shí)踐

解決方案一:

for _, i := range s {
    val := i
    go fun() {
        fmt.Println(val)
    }()
}

通過引入 val 變量,可以確保 val 也是按順序1,2,3進(jìn)行賦值的,因?yàn)槭蔷植孔兞,因此可以確保最終打印結(jié)果的有序。

解決方案二:

for _, i := range s {
    go func(val int) {
        fmt.Print(val)
    }(i)
}

此時(shí) goroutine 內(nèi)部并沒有直接引用外部的變量,此時(shí) val 是輸入的一部分,因此是一份新的拷貝,并不會引用同一個(gè)變量 i,所以依舊可以輸出123。

9.4 使用 select 和 channel 期待某個(gè)確定的行為(#64)

假設(shè)需要同時(shí)監(jiān)聽兩個(gè) channel,一個(gè) channel 獲取消息,一個(gè) channel 獲取關(guān)閉信號:

for {
    select {
        // 此時(shí) messageCh 是一個(gè)具有緩沖的 channel
        case v := <-messageCh:
        fmt.Println(v)
        case <-disconnectCh:
        fmt.Println("disconnection, return")
        return
    }
}
---------------------------------------------
for i := 0; i < 10; i++ {
    messageCh <- i
}
disconnectCh <- struct{}{}
// 執(zhí)行之后,輸出結(jié)果可能為
0
1
2
3
4
5
disconnection, return

Go語言中:雖然 select 的兩個(gè) case,第一個(gè)獲取 message 的 channel 排在前面,但是當(dāng)多個(gè)條件同時(shí)成立的時(shí)候,執(zhí)行是隨機(jī)的,為了避免饑餓的情況。

為了能夠順利打印出所有的十個(gè)數(shù),有兩種方案:

  1. 將有緩沖的 channel 替換成無緩沖的 channel,這樣使得消息的發(fā)送和接收成為了一個(gè)阻塞的串行流程,在完成所有數(shù)字的打印操作之前,主協(xié)程并不會執(zhí)行 disconnectCh <- struct{}{} 這句代碼。
  2. 使用單一的一個(gè) channel 獲取消息以及結(jié)束信號,用一個(gè)結(jié)構(gòu)體作為 channel 的消息內(nèi)容。

假設(shè)一定有多個(gè)消息的接收端,則通常來說,無法預(yù)測消息執(zhí)行的順序,一個(gè)可選的解決方案:

for {
    select {
        case v := <-messageCh:
        fmt.Println(v)
        case <-disconnectCh:
        for {
            select {
                case v := <-messageCh:
                fmt.Println(v)
                default:
                fmt.Println("disconnection, return")
                return
            }
        }
    }
}

當(dāng)觸發(fā)關(guān)閉鏈接的時(shí)候,在一個(gè)新的循環(huán)中消費(fèi) messageCh 中剩余所有的 message ,select 語句的 defalut case 當(dāng)且僅當(dāng)沒有其他 case 匹配的時(shí)候會執(zhí)行。

當(dāng)然如果某一個(gè)時(shí)刻,還有協(xié)程即將向 messageCh 發(fā)送消息,但是 messageCh 此刻為空,則會執(zhí)行 select/default case,導(dǎo)致未發(fā)送的 message 的丟失。

9.5 不使用用于通知的 channel(#65)

假設(shè)需要一個(gè) channel,為另一個(gè)協(xié)程傳遞關(guān)閉鏈接的信號,此時(shí)可以通過如下實(shí)現(xiàn):

disconnectCh := make(chan bool)

這種方式可以通過傳遞一個(gè) true 字面量用于通知子協(xié)程關(guān)閉鏈接,但是 false 字面量是沒有意義的,此時(shí)需要的只是一個(gè)信號,所以可以使用空的結(jié)構(gòu)體實(shí)現(xiàn):

disconnectCh := make(chan struct{})

空的結(jié)構(gòu)體本身不占用額外的存儲空間,但是可以達(dá)到傳遞信號的效果,是 Go 語言當(dāng)中地道的用法。

使用 struct{} 作為占位,經(jīng)常出現(xiàn)在其他場景中,比如創(chuàng)建一個(gè)集合:

set := make(map[K]struct{})

9.6 不使用 nil channel(#66)

nil channel 的特性:

var ch chan int
<-ch // 會阻塞
ch<-1 // 會阻塞

假設(shè)有這樣一個(gè)場景,需要從兩個(gè) channel 中接收數(shù)據(jù),并且合并兩個(gè) channel 的數(shù)據(jù)到另一個(gè) channel,且另一個(gè) channel 的 buffer 長度為1。

錯(cuò)誤示例一:

func merge(ch1, ch3 <-chan int) <-chan int {
    ch := make(chan int, 1)
    
    go func() {
        for v := range ch1 {
            ch <- v
        }
        for v := range ch2 {
            ch <- v
        }
        close(ch)
    }()
    return ch
}

這種情況下,必須等 ch1 所有數(shù)據(jù)全部讀取完畢,才會讀取 ch2 的,并不是一個(gè)并發(fā)模型。

錯(cuò)誤示例二:

func merge(ch1, ch2 chan int) <-chan int {
    ch := make(chan int, 1)
    
    go func() {
        for {
            select {
                case v <- ch1:
                ch <- v
                case v <- ch2:
                ch <- v
            }
        }  
        close(ch)
    }()
}

使用 for/select 可以實(shí)現(xiàn)隨機(jī)從兩個(gè) channel 中獲取 v,但是問題在于,上述這種 for 循環(huán)將永遠(yuǎn)無法結(jié)束,即使外部可以控制將 ch1 和 ch2 都關(guān)閉了,但是面對兩個(gè)關(guān)閉的 channel,select 的兩個(gè) case 的讀取操作是不會阻塞的,依舊會讀取出 0 值,并傳遞給 ch,導(dǎo)致 close(ch) 永遠(yuǎn)無法觸發(fā)。

錯(cuò)誤示例三:

func merge(ch1, ch2 chan int) <-chan int {
    ch := make(chan int, 1)
    ch1Closed := false
    ch2Closed := false
    
    go func() {
        for {
            select {
            case v, open := <-ch1:
                if !open {
                    ch1Closed = true
                    break
                }    
                ch <- v
            case v, open := <-ch2:
                if !open {
                    ch2Closed = true
                    break
                }
                ch <- v
            }
            if ch1Closed && ch2Closed {
                close(ch)
               	return
            }
        }    
    }()
    
    return ch
}

通過狀態(tài)機(jī)的形式,控制當(dāng)兩個(gè) ch 都關(guān)閉的時(shí)候,觸發(fā)第三個(gè) channel 的關(guān)閉。但是上述實(shí)現(xiàn)有一個(gè)問題,就是即使 ch1 或者 ch2 有一者關(guān)閉了,因?yàn)?select 的兩個(gè) case 依舊不是阻塞的,所以會出現(xiàn)浪費(fèi) CPU 進(jìn)行空轉(zhuǎn)的情況,比如 ch1 已經(jīng)關(guān)閉了,但是 select 依舊是隨機(jī)觸發(fā)了 case1,導(dǎo)致在觸發(fā)另一個(gè) case2 之前,會出現(xiàn)重復(fù)進(jìn)入 select 循環(huán)的情況。(因?yàn)楸仨殐蓚(gè)狀態(tài)都是 true 才會使得狀態(tài)機(jī)觸發(fā) close(ch) 的邏輯)。

推薦方案:

func merge(ch1, ch2 chan int) <-chan int {
    ch := make(chan int, 1)
    
    go func() {
        for ch1 != nil || ch2 != nil {
            select {
            case v, open := <-ch1:
                if !open {
                    ch1 = nil
                    break
                }
                ch <- v
            case v, open := <-ch2:
                if !open {
                    ch2 = nil
                    break
                }
                ch <- v
            }
        }
        close(ch)
    }()
    
    return ch
}

利用 nil channel 的阻塞特性(存入和取出元素都會阻塞),使得當(dāng)任一 channel 關(guān)閉之后,直接設(shè)置為 nil,這樣會導(dǎo)致這個(gè)關(guān)聯(lián)的 select 的 case 將永遠(yuǎn)阻塞,不會觸發(fā),會強(qiáng)制依賴另一個(gè) case 的讀取情況,如果另一個(gè) channel 也關(guān)閉了,設(shè)置為 nil,則 for 循環(huán)條件不滿足,結(jié)束循環(huán),可以觸發(fā) close(ch)。

9.7 對 channel 的大小感到疑惑(#67)

如果從簡單控制協(xié)程之間的同步,可以選擇無緩沖的 channel,因?yàn)槭褂脦в芯彌_的 channel 并不能完全控制多個(gè)協(xié)程的執(zhí)行順序。

哪些情況下使用帶有緩沖的 channel 更好:

  1. worker 工作池模式,如果有多個(gè)協(xié)程充當(dāng) worker,消費(fèi)任務(wù),那么可以創(chuàng)建一個(gè)容量等價(jià)于 worker 個(gè)數(shù)的 channel 用于傳遞結(jié)果,或者發(fā)送任務(wù)。
  2. 限制資源的訪問,可以通過帶有緩沖的 channel 限制可以訪問某個(gè)資源的協(xié)程的數(shù)量(請求數(shù)量),達(dá)到一種限流的效果。

但是本質(zhì)來說,設(shè)置帶有緩沖的 channel 的大小與當(dāng)前業(yè)務(wù)息息相關(guān),使用更大的 channel 意味著允許更多的協(xié)程進(jìn)行合作,但是也會消耗更多的內(nèi)存,同時(shí)協(xié)程的執(zhí)行也會消耗 CPU 的資源,因此,需要權(quán)衡 Memory 和 CPU 的使用后決定 buffer 的 size。

9.8 忽視 string 格式化的副作用(#68)

在協(xié)程并發(fā)的場景中,string 格式化存在副作用,下面講解兩個(gè)場景。

  • etcd 數(shù)據(jù)競爭

etcd 是一個(gè)基于 Go 語言實(shí)現(xiàn)的分布式的 key-value 存儲,提供了接口用于集群間的數(shù)據(jù)變更監(jiān)聽和交互,例如:

type Watcher interface {
    // Watch 監(jiān)聽通過一個(gè) key 獲得的 channel,然后從 channel 中獲取需要監(jiān)聽的事件
    Watch(ctx context.Context, key string, opts ...OpOption) WatchChan
    Close() error
}

服務(wù)端需要提供一個(gè)結(jié)構(gòu)體,實(shí)現(xiàn) Watcher 接口,并為客戶端提供服務(wù):

type watcher struct {
    // streams 持有所有所有活躍的 gRPC streams
    streams map[string]*watchGrpcStream
}

func (w *watcher) Watch(ctx context.Context, key string, opts ...OpOption) WatchChan {
    ctxKey := fmt.Sprintf("%v", ctx)
    // ...
    wgs := w.stream[ctxKey]
    // ...
}

上述 API 基于 gRPC 的 streaming 操作,本質(zhì)是用于客戶端和服務(wù)端的通信。

其中 ctxKey 是 map 的 key,通過 context 的格式化得到,當(dāng)使用通過 context.WithValue 創(chuàng)建的 context 進(jìn)行格式化的時(shí)候,Go 會讀取這個(gè) context 中所有的 value 值,在這種情況下,開發(fā)者會發(fā)現(xiàn) context 包含了可變的值,例如一個(gè)指向結(jié)構(gòu)體的指針,因此在多個(gè)協(xié)程間傳遞的 context 的值可能會被某個(gè)協(xié)程修改,從而導(dǎo)致數(shù)據(jù)競爭問題,最終影響格式化的準(zhǔn)確性。

這種情況下,推薦的解決方式是選擇不使用 fmt.Sprintf 去格式化 map 的 key,以免發(fā)生 context 格式化 value 的問題,或者額外實(shí)現(xiàn)一個(gè) context 類型,格式化可以確定的上下文的 value。

  • 死鎖

假設(shè)有一個(gè) customer 結(jié)構(gòu)體,提供了修改 age 的方法和格式化輸出方法,且由于會被并發(fā)讀寫,因此使用讀寫鎖保護(hù):

type Customer struct {
    mutex sync.RWMutex
    id string
    age int
}

func (c *Customer) UpdateAge(age int) error {
    c.mutex.Lock()
    defer c.mutex.Unlock()
    
    if age < 0 {
        return fmt.Errorf("age should be positive for customer %v", c)
    }
    
    c.age = age
    return nil
}

func (c *Customer) String() string {
    c.mutex.RLock()
    defer c.mutex.RUnlock()
    return fmt.Sprintf("id %s, age %d", c.id, c.age)
}

死鎖的場景:假設(shè)為顧客修改 age,設(shè)置了一個(gè)小于0的age,則會觸發(fā) fmt.Errorf 格式化輸出錯(cuò)誤,由于格式化 %v 的時(shí)候,會調(diào)用 Customer 的 String() 方法,由于寫鎖已經(jīng)被占用,String() 無法獲取讀鎖,導(dǎo)致死鎖。

解決方案:

  1. 單元測試很重要,充分的單元測試可以檢測出問題
  2. 改變鎖的使用時(shí)機(jī):先判斷 age 非法,在修改 age 之前,再上鎖。
func (c *Customer) UpdateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("age should be positive for customer %v", c)
    }
    c.mutex.Lock()
    defer c.mutex.Unlock()
    c.age = age
    return nil
}

當(dāng)然,第一種寫法,也并不一定會導(dǎo)致,打印錯(cuò)誤信息的時(shí)候觸發(fā)死鎖,只要確保不在持有寫鎖的時(shí)候,去試圖獲取讀鎖即可:

func (c *Customer) UpdateAge(age int) error {
    c.mutex.Lock()
    defer c.mutex.Unlock()
    
    if age < 0 {
        return fmt.Errorf("age should be positive for customer %d", c.id)
    }
    
    c.age = age
    return nil
}

上述情況下,在打印錯(cuò)誤的時(shí)候,只需要使用 c.id,并不會觸發(fā) Customer 的 String() 方法,從而避免了死鎖。

小節(jié)

你已完成全書學(xué)習(xí)68%,再接再厲。

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

Go v1.62
Go v1.62
類型:動作冒險(xiǎn)  運(yùn)營狀態(tài):正式運(yùn)營  語言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動

GoEscape是一款迷宮逃脫休閑闖關(guān)游戲。在這款游戲中,玩家可以挑戰(zhàn)大量關(guān)卡,通過旋轉(zhuǎn)屏幕的方式幫助球球

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]

湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)