Go開(kāi)發(fā)關(guān)鍵技術(shù)指南:Errors

Errors

錯(cuò)誤處理是現(xiàn)實(shí)中經(jīng)常碰到的竟贯、難以處理好的問(wèn)題扶踊,下面會(huì)從下面幾個(gè)方面探討錯(cuò)誤處理:

錯(cuò)誤和異常

我們總會(huì)遇到非預(yù)期的非正常情況顶掉,有一種是符合預(yù)期的草娜,比如函數(shù)返回error并處理,這種叫做可以預(yù)見(jiàn)到的錯(cuò)誤痒筒,還有一種是預(yù)見(jiàn)不到的比如除零宰闰、空指針、數(shù)組越界等叫做panic簿透,panic的處理主要參考Defer, Panic, and Recover移袍。

錯(cuò)誤處理的模型一般有兩種,一般是錯(cuò)誤碼模型比如C/C++和Go老充,還有異常模型比如Java和C#葡盗。Go沒(méi)有選擇異常模型,因?yàn)殄e(cuò)誤碼比異常更有優(yōu)勢(shì)啡浊,參考文章Cleaner, more elegant, and wrong 以及Cleaner, more elegant, and harder to recognize觅够。看下面的代碼:

try {
  AccessDatabase accessDb = new AccessDatabase();
  accessDb.GenerateDatabase();
} catch (Exception e) {
  // Inspect caught exception
}

public void GenerateDatabase()
{
  CreatePhysicalDatabase();
  CreateTables();
  CreateIndexes();
}

這段代碼的錯(cuò)誤處理有很多問(wèn)題巷嚣,比如如果CreateIndexes拋出異常喘先,會(huì)導(dǎo)致數(shù)據(jù)庫(kù)和表不會(huì)刪除,造成臟數(shù)據(jù)廷粒。從代碼編寫(xiě)者和維護(hù)者的角度看這兩個(gè)模型窘拯,會(huì)比較清楚:

Really Easy Hard Really Hard
Writing bad error-code-based code
Writing bad exception-based code
Writing good
error-code-based code
Writing good
exception-based code

錯(cuò)誤處理不容易做好,要說(shuō)容易那說(shuō)明做錯(cuò)了坝茎;要把錯(cuò)誤處理寫(xiě)對(duì)了涤姊,基于錯(cuò)誤碼模型雖然很難,但比異常模型簡(jiǎn)單嗤放。

Really Easy Hard Really Hard
Recognizing that error-code-based
code is badly-written
Recognizing the difference
between bad error-code-based
code and not-bad
error-code-based code.
Recognizing that
error-code-base code
is not badly-written
Recognizing that
exception-based code
is badly-written
Recognizing that
exception-based code
is not badly-written
Recognizing
the difference between
bad exception-based code
and
not-bad exception-based code

如果使用錯(cuò)誤碼模型砂轻,非常容易就能看出錯(cuò)誤處理沒(méi)有寫(xiě)對(duì),也能很容易知道做得好不好斤吐;要知道是否做得非常好搔涝,錯(cuò)誤碼模型也不太容易。
如果使用異常模型和措,無(wú)論做的好不好都很難知道庄呈,而且也很難知道怎么做好。

Errors in Go

Go官方的error介紹派阱,簡(jiǎn)單一句話(huà)就是返回錯(cuò)誤對(duì)象的方式诬留,參考Error handling and Go,解釋了error是什么,如何判斷具體的錯(cuò)誤文兑,顯式返回錯(cuò)誤的好處盒刚。文中舉的例子就是打開(kāi)文件錯(cuò)誤:

func Open(name string) (file *File, err error)

Go可以返回多個(gè)值,最后一個(gè)一般是error绿贞,我們需要檢查和處理這個(gè)錯(cuò)誤因块,這就是Go的錯(cuò)誤處理的官方介紹:

if err := Open("src.txt"); err != nil {
    // Handle err
}

看起來(lái)非常簡(jiǎn)單的錯(cuò)誤處理,有什么難的呢籍铁?騷等涡上,在Go2的草案中,提到的三個(gè)點(diǎn)Error Handling拒名、Error ValuesGenerics泛型吩愧,兩個(gè)點(diǎn)都是錯(cuò)誤處理的,這說(shuō)明了Go1中對(duì)于錯(cuò)誤是有改進(jìn)的地方增显。

再詳細(xì)看下Go2的草案雁佳,錯(cuò)誤處理:Error Handling中,主要描述了發(fā)生錯(cuò)誤時(shí)的重復(fù)代碼同云,以及不能便捷處理錯(cuò)誤的情況甘穿。比如草案中舉的這個(gè)例子No Error Handling: CopyFile,沒(méi)有做任何錯(cuò)誤處理:

package main

import (
  "fmt"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, _ := os.Open(src)
  defer r.Close()

  w, _ := os.Create(dst)
  io.Copy(w, r)
  w.Close()

  return nil
}

func main() {
  fmt.Println(CopyFile("src.txt", "dst.txt"))
}

還有草案中這個(gè)例子Not Nice and still Wrong: CopyFile梢杭,錯(cuò)誤處理是特別啰嗦温兼,而且比較明顯有問(wèn)題:

package main

import (
  "fmt"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer r.Close()

  w, err := os.Create(dst)
  if err != nil {
    return err
  }
  defer w.Close()

  if _, err := io.Copy(w, r); err != nil {
    return err
  }
  if err := w.Close(); err != nil {
    return err
  }
  return nil
}

func main() {
  fmt.Println(CopyFile("src.txt", "dst.txt"))
}

當(dāng)io.Copyw.Close出現(xiàn)錯(cuò)誤時(shí),目標(biāo)文件實(shí)際上是有問(wèn)題武契,那應(yīng)該需要?jiǎng)h除dst文件的募判。而且需要給出錯(cuò)誤時(shí)的信息,比如是哪個(gè)文件咒唆,不能直接返回err届垫。所以Go中正確的錯(cuò)誤處理,應(yīng)該是這個(gè)例子Good: CopyFile全释,雖然啰嗦繁瑣不簡(jiǎn)潔:

package main

import (
  "fmt"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, err := os.Open(src)
  if err != nil {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }
  defer r.Close()

  w, err := os.Create(dst)
  if err != nil {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }

  if _, err := io.Copy(w, r); err != nil {
    w.Close()
    os.Remove(dst)
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }

  if err := w.Close(); err != nil {
    os.Remove(dst)
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }
  return nil
}

func main() {
  fmt.Println(CopyFile("src.txt", "dst.txt"))
}

具體應(yīng)該如何簡(jiǎn)潔的處理錯(cuò)誤装处,可以讀Error Handling,大致是引入關(guān)鍵字handle和check浸船,由于本文重點(diǎn)側(cè)重Go1如何錯(cuò)誤處理妄迁,就不展開(kāi)分享了。

明顯上面每次都返回的fmt.Errorf信息也是不夠的李命,所以Go2還對(duì)于錯(cuò)誤的值有提案登淘,參考Error Values。大規(guī)模程序應(yīng)該面向錯(cuò)誤編程和測(cè)試封字,同時(shí)錯(cuò)誤應(yīng)該包含足夠的信息黔州。Go1中判斷error具體是什么錯(cuò)誤有幾種辦法:

  • 直接比較耍鬓,比如返回的是io.EOF這個(gè)全局變量,那么可以直接比較是否是這個(gè)錯(cuò)誤流妻。
  • 可以用類(lèi)型轉(zhuǎn)換type或switch牲蜀,嘗試來(lái)轉(zhuǎn)換成具體的錯(cuò)誤類(lèi)型,看是哪種錯(cuò)誤绅这。
  • 提供某些函數(shù)來(lái)判斷是否是某個(gè)錯(cuò)誤涣达,比如os.IsNotExist判斷是否是指定錯(cuò)誤。
  • 當(dāng)多個(gè)錯(cuò)誤被糅合到一起時(shí)君躺,只能用error.Error()返回的字符串匹配峭判,看是否是某個(gè)錯(cuò)誤开缎。

在復(fù)雜程序中棕叫,有用的錯(cuò)誤需要包含調(diào)用鏈的信息。例如奕删,考慮一次數(shù)據(jù)庫(kù)寫(xiě)俺泣,可能調(diào)用了RPC,RPC調(diào)用了域名解析完残,最終是沒(méi)有權(quán)限讀/etc/resolve.conf文件伏钠,那么給出下面的調(diào)用鏈會(huì)非常有用:

write users database: call myserver.Method: \
    dial myserver:3333: open /etc/resolv.conf: permission denied

Errors Solutions

由于Go1的錯(cuò)誤值沒(méi)有完整的解決這個(gè)問(wèn)題,才導(dǎo)致出現(xiàn)非常多的錯(cuò)誤處理的庫(kù)谨设,比如:

  • 2017, 12, upspin.io/errors熟掂,帶邏輯調(diào)用堆棧的錯(cuò)誤庫(kù),而不是執(zhí)行的堆棧扎拣,引入了errors.Is赴肚、errors.Aserrors.Match
  • 2015.12, github.com/pkg/errors二蓝,帶堆棧的錯(cuò)誤誉券,引入了%+v來(lái)格式化錯(cuò)誤的額外信息比如堆棧。
  • 2014.10, github.com/hashicorp/errwrap刊愚,可以wrap多個(gè)錯(cuò)誤踊跟,引入了錯(cuò)誤樹(shù),提供Walk函數(shù)遍歷所有的錯(cuò)誤鸥诽。
  • 2014.2, github.com/juju/errgo商玫,Wrap時(shí)可以選擇是否隱藏底層錯(cuò)誤。和pkg/errors的Cause返回最底層的錯(cuò)誤不同牡借,它只反饋錯(cuò)誤鏈的下一個(gè)錯(cuò)誤决帖。
  • 2013.7, github.com/spacemonkeygo/errors,是來(lái)源于一個(gè)大型Python項(xiàng)目蓖捶,有錯(cuò)誤的hierarchies地回,自動(dòng)記錄日志和堆棧,還可以帶額外的信息。打印錯(cuò)誤的消息比較固定刻像,不能自己定義畅买。
  • 2019.09,Go1.13標(biāo)準(zhǔn)庫(kù)擴(kuò)展了error细睡,支持了Unwrap谷羞、As和Is,但沒(méi)有支持堆棧信息溜徙。

Go1.13改進(jìn)了errors湃缎,參考如下實(shí)例代碼:

package main

import (
    "errors"
    "fmt"
    "io"
)

func foo() error {
    return fmt.Errorf("read err: %w", io.EOF)
}

func bar() error {
    if err := foo(); err != nil {
        return fmt.Errorf("foo err: %w", err)
    }
    return nil
}

func main() {
    if err := bar(); err != nil {
        fmt.Printf("err: %+v\n", err)
        fmt.Printf("unwrap: %+v\n", errors.Unwrap(err))
        fmt.Printf("unwrap of unwrap: %+v\n", errors.Unwrap(errors.Unwrap(err)))
        fmt.Printf("err is io.EOF? %v\n", errors.Is(err, io.EOF))
    }
}

運(yùn)行結(jié)果如下:

err: foo err: read err: EOF
unwrap: read err: EOF
unwrap of unwrap: EOF
err is io.EOF? true

從上面的例子可以看出:

  • 沒(méi)有堆棧信息,主要是想通過(guò)Wrap的日志來(lái)標(biāo)識(shí)堆棧蠢壹,如果全部Wrap一層和堆棧差不多嗓违,不過(guò)對(duì)于沒(méi)有Wrap的錯(cuò)誤還是無(wú)法知道調(diào)用堆棧。
  • Unwrap只會(huì)展開(kāi)第一個(gè)嵌套的error图贸,如果錯(cuò)誤有多層嵌套蹂季,取不到最里面的那個(gè)error,需要多次Unwrap才行疏日。
  • errors.Is能判斷出是否是最里面的那個(gè)error偿洁。

另外,錯(cuò)誤處理往往和log是容易混為一談的沟优,因?yàn)橛龅藉e(cuò)誤一般會(huì)打日志涕滋,特別是在C/C++中返回錯(cuò)誤碼一般都會(huì)打日志記錄下,有時(shí)候還會(huì)記錄一個(gè)全局的錯(cuò)誤碼比如linux的errno挠阁,而這種習(xí)慣宾肺,造成了error和log混淆造成比較大的困擾【槲ǎ考慮以前寫(xiě)了一個(gè)C++的服務(wù)器爱榕,出現(xiàn)錯(cuò)誤時(shí)會(huì)在每一層打印日志,所以就會(huì)形成堆棧式的錯(cuò)誤日志坡慌,便于排查問(wèn)題黔酥,如果只有一個(gè)錯(cuò)誤,不知道調(diào)用上下文洪橘,排查會(huì)很困難:

avc decode avc_packet_type failed. ret=3001
Codec parse video failed, ret=3001
origin hub error, ret=3001

這種比只打印一條日志origin hub error, ret=3001要好跪者,但是還不夠好:

  1. 和Go的錯(cuò)誤一樣,比較啰嗦熄求,有重復(fù)的信息渣玲。如果能提供堆棧信息,可以省去很多需要手動(dòng)寫(xiě)的信息弟晚。
  2. 對(duì)于應(yīng)用程序可以打日志忘衍,但是對(duì)于庫(kù)逾苫,信息都應(yīng)該包含在error中,不應(yīng)該直接打印日志枚钓。如果底層的庫(kù)都要打印日志铅搓,那會(huì)導(dǎo)致底層庫(kù)都要依賴(lài)日志庫(kù),這是很多庫(kù)都有日志打印函數(shù)供調(diào)用者重寫(xiě)搀捷。
  3. 對(duì)于多線(xiàn)程星掰,看不到線(xiàn)程信息,或者看不到業(yè)務(wù)層ID的信息嫩舟。對(duì)于服務(wù)器來(lái)說(shuō)氢烘,有時(shí)候需要知道這個(gè)錯(cuò)誤是哪個(gè)連接的,從而查詢(xún)這個(gè)連接之前的上下文信息家厌。

改進(jìn)后的錯(cuò)誤日志變成了在底層返回播玖,而不在底層打印在調(diào)用層打印,有調(diào)用鏈和堆棧像街,有線(xiàn)程切換的ID信息黎棠,也有文件的行數(shù):

Error processing video, code=3001 : origin hub : codec parser : avc decoder
[100] video_avc_demux() at [srs_kernel_codec.cpp:676]
[100] on_video() at [srs_app_source.cpp:1076]
[101] on_video_imp() at [srs_app_source:2357]

從Go2的描述來(lái)說(shuō)晋渺,實(shí)際上這個(gè)錯(cuò)誤處理也還沒(méi)有考慮完備镰绎。從實(shí)際開(kāi)發(fā)來(lái)說(shuō),已經(jīng)比較實(shí)用了木西。

總結(jié)下Go的error畴栖,錯(cuò)誤處理應(yīng)該注意的點(diǎn):

  1. 凡是有返回錯(cuò)誤碼的函數(shù),必須顯式的處理錯(cuò)誤八千,如果要忽略錯(cuò)誤吗讶,也應(yīng)該顯式的忽略和寫(xiě)注釋。
  2. 錯(cuò)誤必須帶豐富的錯(cuò)誤信息恋捆,比如堆棧照皆,發(fā)生錯(cuò)誤時(shí)的參數(shù),調(diào)用鏈給的描述等等沸停。特別要強(qiáng)調(diào)變量膜毁,我看過(guò)太多日志描述了一對(duì)常量,比如"Verify the nonce, timestamp and token of specified appid failed"愤钾,而這個(gè)消息一般會(huì)提到工單中瘟滨,然后就是再問(wèn)用戶(hù),哪個(gè)session或request甚至?xí)r間點(diǎn)能颁?這么一大堆常量有啥用呢杂瘸,關(guān)鍵是變量,關(guān)鍵是變量吶伙菊。
  3. 盡量避免重復(fù)的信息败玉,提高錯(cuò)誤處理的開(kāi)發(fā)體驗(yàn)敌土,糟糕的體驗(yàn)會(huì)導(dǎo)致無(wú)效的錯(cuò)誤處理代碼比如拷貝和漏掉關(guān)鍵信息。
  4. 分離錯(cuò)誤和日志运翼,發(fā)生錯(cuò)誤時(shí)返回帶完整信息的錯(cuò)誤纯赎,在調(diào)用的頂層決定是將錯(cuò)誤用日志打印,還是發(fā)送到監(jiān)控系統(tǒng)南蹂,還是轉(zhuǎn)換錯(cuò)誤犬金,或者忽略。

Best Practice

推薦用github.com/pkg/errors這個(gè)錯(cuò)誤處理的庫(kù)六剥,基本上是夠用的晚顷,參考Refine: CopyFile,可以看到CopyFile中低級(jí)重復(fù)的代碼已經(jīng)比較少了:

package main

import (
  "fmt"
  "github.com/pkg/errors"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, err := os.Open(src)
  if err != nil {
    return errors.Wrap(err, "open source")
  }
  defer r.Close()

  w, err := os.Create(dst)
  if err != nil {
    return errors.Wrap(err, "create dest")
  }

  nn, err := io.Copy(w, r)
  if err != nil {
    w.Close()
    os.Remove(dst)
    return errors.Wrap(err, "copy body")
  }

  if err := w.Close(); err != nil {
    os.Remove(dst)
    return errors.Wrapf(err, "close dest, nn=%v", nn)
  }

  return nil
}

func LoadSystem() error {
  src, dst := "src.txt", "dst.txt"
  if err := CopyFile(src, dst); err != nil {
    return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst))
  }

  // Do other jobs.

  return nil
}

func main() {
  if err := LoadSystem(); err != nil {
    fmt.Printf("err %+v\n", err)
  }
}

改寫(xiě)的函數(shù)中疗疟,用errors.Wraperrors.Wrapf代替了fmt.Errorf该默,我們不記錄src和dst的值,因?yàn)樵谏蠈訒?huì)記錄這個(gè)值(參考下面的代碼)策彤,而只記錄我們這個(gè)函數(shù)產(chǎn)生的數(shù)據(jù)栓袖,比如nn

import "github.com/pkg/errors"

func LoadSystem() error {
    src, dst := "src.txt", "dst.txt"
    if err := CopyFile(src, dst); err != nil {
        return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst))
    }

    // Do other jobs.

    return nil
}

在這個(gè)上層函數(shù)中店诗,我們用的是errors.WithMessage添加了這一層的錯(cuò)誤信息裹刮,包括srcdst,所以CopyFile里面就不用重復(fù)記錄這兩個(gè)數(shù)據(jù)了庞瘸。同時(shí)我們也沒(méi)有打印日志捧弃,只是返回了帶完整信息的錯(cuò)誤。

func main() {
    if err := LoadSystem(); err != nil {
        fmt.Printf("err %+v\n", err)
    }
}

在頂層調(diào)用時(shí)擦囊,我們拿到錯(cuò)誤违霞,可以決定是打印還是忽略還是送監(jiān)控系統(tǒng)。

比如我們?cè)谡{(diào)用層打印錯(cuò)誤瞬场,使用%+v打印詳細(xì)的錯(cuò)誤买鸽,有完整的信息:

err open src.txt: no such file or directory
open source
main.CopyFile
    /Users/winlin/t.go:13
main.LoadSystem
    /Users/winlin/t.go:39
main.main
    /Users/winlin/t.go:49
runtime.main
    /usr/local/Cellar/go/1.8.3/libexec/src/runtime/proc.go:185
runtime.goexit
    /usr/local/Cellar/go/1.8.3/libexec/src/runtime/asm_amd64.s:2197
load src=src.txt, dst=dst.txt

但是這個(gè)庫(kù)也有些小毛病:

  1. CopyFile中還是有可能會(huì)有重復(fù)的信息贯被,還是Go2的handlecheck方案是最終解決眼五。
  2. 有時(shí)候需要用戶(hù)調(diào)用Wrap,有時(shí)候是調(diào)用WithMessage(不需要加堆棧時(shí))刃榨,這個(gè)真是非常不好用的地方(這個(gè)我們已經(jīng)修改了庫(kù)弹砚,可以全部使用Wrap不用WithMessage,會(huì)去掉重復(fù)的堆棧)枢希。

Links

由于簡(jiǎn)書(shū)限制了文章字?jǐn)?shù)桌吃,只好分成不同章節(jié):

  • Overview 為何Go有時(shí)候也叫Golang?為何要選擇Go作為服務(wù)器開(kāi)發(fā)的語(yǔ)言?是沖動(dòng)苞轿?還是騷動(dòng)茅诱?Go的重要里程碑和事件逗物,當(dāng)年吹的那些牛逼,都實(shí)現(xiàn)了哪些瑟俭?
  • Could Not Recover 君可知翎卓,有什么panic是無(wú)法recover的?包括超過(guò)系統(tǒng)線(xiàn)程限制摆寄,以及map的競(jìng)爭(zhēng)寫(xiě)失暴。當(dāng)然一般都能recover,比如Slice越界微饥、nil指針逗扒、除零、寫(xiě)關(guān)閉的chan等欠橘。
  • Errors 為什么Go2的草稿3個(gè)有2個(gè)是關(guān)于錯(cuò)誤處理的矩肩?好的錯(cuò)誤處理應(yīng)該怎么做?錯(cuò)誤和異常機(jī)制的差別是什么肃续?錯(cuò)誤處理和日志如何配合黍檩?
  • Logger 為什么標(biāo)準(zhǔn)庫(kù)的Logger是完全不夠用的?怎么做日志切割和輪轉(zhuǎn)始锚?怎么在混成一坨的服務(wù)器日志中找到某個(gè)連接的日志刽酱?甚至連接中的流的日志?怎么做到簡(jiǎn)潔又夠用疼蛾?
  • Interfaces 什么是面向?qū)ο蟮腟OLID原則肛跌?為何Go更符合SOLID艺配?為何接口組合比繼承多態(tài)更具有正交性察郁?Go類(lèi)型系統(tǒng)如何做到looser, organic, decoupled, independent, and therefore scalable?一般軟件中如果出現(xiàn)數(shù)學(xué)转唉,要么真的牛逼要么裝逼皮钠。正交性這個(gè)數(shù)學(xué)概念在Go中頻繁出現(xiàn),是神仙還是妖怪赠法?為何接口設(shè)計(jì)要考慮正交性麦轰?
  • Modules 如何避免依賴(lài)地獄(Dependency Hell)?小小的版本號(hào)為何會(huì)帶來(lái)大災(zāi)難砖织?Go為什么推出了GOPATH款侵、Vendor還要搞module和vgo?新建了16個(gè)倉(cāng)庫(kù)做測(cè)試侧纯,碰到了9個(gè)坑新锈,搞清楚了gopath和vendor如何遷移,以及vgo with vendor如何使用(畢竟生產(chǎn)環(huán)境不能每次都去外網(wǎng)下載)眶熬。
  • Concurrency & Control 服務(wù)器中的并發(fā)處理難在哪里妹笆?為什么說(shuō)Go并發(fā)處理優(yōu)勢(shì)占領(lǐng)了云計(jì)算開(kāi)發(fā)語(yǔ)言市場(chǎng)?什么是C10K、C10M問(wèn)題贵白?如何管理goroutine的取消马澈、超時(shí)和關(guān)聯(lián)取消?為何Go1.7專(zhuān)門(mén)將context放到了標(biāo)準(zhǔn)庫(kù)窟坐?context如何使用海渊,以及問(wèn)題在哪里?
  • Engineering Go在工程化上的優(yōu)勢(shì)是什么哲鸳?為什么說(shuō)Go是一門(mén)面向工程的語(yǔ)言切省?覆蓋率要到多少比較合適?什么叫代碼可測(cè)性帕胆?為什么良好的庫(kù)必須先寫(xiě)Example朝捆?
  • Go2 Transition Go2會(huì)像Python3不兼容Python2那樣作嗎?C和C++的語(yǔ)言演進(jìn)可以有什么不同的收獲懒豹?Go2怎么思考語(yǔ)言升級(jí)的問(wèn)題芙盘?
  • SRS & Others Go在流媒體服務(wù)器中的使用。Go的GC靠譜嗎脸秽?Twitter說(shuō)相當(dāng)?shù)目孔V儒老,有圖有真相。為何Go的聲明語(yǔ)法是那樣记餐?C的又是怎樣驮樊?是拍的大腿,還是拍的腦袋片酝?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載囚衔,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末雕沿,一起剝皮案震驚了整個(gè)濱河市练湿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌审轮,老刑警劉巖肥哎,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異疾渣,居然都是意外死亡篡诽,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)榴捡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杈女,“玉大人,你說(shuō)我怎么就攤上這事”绦牛” “怎么了赊琳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)砰碴。 經(jīng)常有香客問(wèn)我躏筏,道長(zhǎng),這世上最難降的妖魔是什么呈枉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任趁尼,我火速辦了婚禮,結(jié)果婚禮上猖辫,老公的妹妹穿的比我還像新娘酥泞。我一直安慰自己,他們只是感情好啃憎,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布芝囤。 她就那樣靜靜地躺著,像睡著了一般辛萍。 火紅的嫁衣襯著肌膚如雪悯姊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天贩毕,我揣著相機(jī)與錄音悯许,去河邊找鬼。 笑死辉阶,一個(gè)胖子當(dāng)著我的面吹牛先壕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谆甜,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼垃僚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了店印?” 一聲冷哼從身側(cè)響起冈在,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎按摘,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體纫谅,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡炫贤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了付秕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兰珍。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖询吴,靈堂內(nèi)的尸體忽然破棺而出掠河,到底是詐尸還是另有隱情亮元,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布唠摹,位于F島的核電站爆捞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏勾拉。R本人自食惡果不足惜煮甥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望藕赞。 院中可真熱鬧成肘,春花似錦、人聲如沸斧蜕。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)批销。三九已至店煞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間风钻,已是汗流浹背顷蟀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骡技,地道東北人鸣个。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像布朦,于是被迫代替她去往敵國(guó)和親囤萤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容