速讀《effective go》

1. 介紹

2. 格式化

3. 注釋

4. 命名

5. 分號

6. 控制結構

7. 函數

8. 數據

9. 初始化

10. 方法

11. 接口和其它類型

12. 空白標識符

13. 內嵌

14. 并發

15. 錯誤處理

1. 介紹[1]

本文檔提供了編寫清晰、慣用的GO代碼的技巧。

2. 格式化[2]

gofmt自動生成統一風格代碼格式,程序員無需關心代碼格式問題。

3. 注釋[3]

Go提供C風格的塊注釋/* */和C++風格的行注釋//。一般使用行注釋,包注釋使用塊注釋。

4. 命名[4]

首字母大寫的命名包外可見。

包名

包名應該簡潔明了,便于記憶。建議是一個小寫單詞,不包含下劃線和混合大小寫。
import "src/encoding/base64"后,導入包用base64代替。這使得包中導出名字避免冗余,bufio.Readerbufio.BufReader相比,更簡潔明了。

Getters

小寫命名做為包內成員變量,大寫命名做為公開成員讀方法,公開成員寫方法。例如,owner為包內成員變量,Owner()為公開方法返回ownerSetOwner()為公開方法修改owner變量。

接口命名

單方法接口采用方法名加er后綴方式命名,例如ReaderWrtier。除非用途和簽名完全一致,不要采用ReadWriteString等系統保留方法名。轉換字符串的方法名為String而非ToString

多詞命名

采用MixedCaps或mixedCaps風格,而非下劃線。

5. 分號[5]

大多數情況下不需要輸入分號,go語法分析器會自動插入。for語句或多條語句在一行時,需要輸入分號分割語句。自動插入的一個副作用是左花括號不能在一行開頭。

6. 控制結構[6]

If

if可以接受一個初始語句

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

重聲明和重賦值

f, err := os.Open(name)這條語句聲明并賦值兩個變量ferr。符合下面條件時:=可對已存在變量v重賦值。

  • 同一作用域中v已存在。(否則在不同作用域中聲明新變量v
  • 右邊值可以正確賦給v
  • 至少產生一個新聲明變量

例如,d, err := f.Stat()
go語言中,函數參數和返回值同函數體具有相同的作用域。

For

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

針對集合對象,使用range

for key, value := range oldMap {
    newMap[key] = value
}

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

sum := 0
for _, value := range array {
    sum += value
}

go不支持++--運算,但支持多賦值

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Switch

go的switch表達式可以不為常量甚至不為數字,依次比較每個case直到匹配。表達式為空意味匹配true

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

匹配成功后就會返回,case子句支持逗號分隔。

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

break在swtich中并不常見,但也可以和label配合,直接跳到外層。

Loop:
    for n := 0; n < len(src); n += size {
        switch {
        case src[n] < sizeOne:
            if validateOnly {
                break
            }
            size = 1
            update(src[n])

        case src[n] < sizeTwo:
            if n+1 >= len(src) {
                err = errShortInput
                break Loop
            }
            if validateOnly {
                break
            }
            size = 2
            update(src[n] + src[n+1]<<shift)
        }
    }

Type Swtich

switch也可以用來判斷一個變量的類型,switch表達式中聲明一個變量,它在每個case子句中具有相應的類型。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

7. 函數[7]

多返回值

go函數和方法支持多返回值。

命名結果參數

返回值可以命名,并像輸入參數一樣使用。它們的初始值為0。

Defer

defer調用一個函數,使其在被調用函數返回前運行。defer的典型用法是釋放資源。defer函數的參數是在defer調用時賦值并被保持住。defer調用的執行順序是后進先出。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

上面代碼的運行結果是4 3 2 1 0

8. 數據[8]

new分配

new(T)返回類型T的新分配的0值對象指針*T

構造器和對象構造方法(復合文字)

構造器就是對象工廠方法分配對象并進行初始化工作。

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    return File{fd, name, nil, 0}
}

go可以返回臨時對象指針(go采用垃圾回收機制)。默認對象構造方法必須依次列舉所有的成員。采用field:name對方式,可以只列舉需要初始化的成員,return &File{fd: fd, name: name}new(File)&File{}相同

make分配

make只能用來創建slicemapchannel,并返回初始化號的對象(不是對象指針)。例如
make([]int, 10, 100)生成一個slice對象,長度是10,容量是100,并指向一個長度為100的int數組。相反,new([]int)返回的0值slice指針并不能使用。

數組

go數組定義需要制定大小,并且大小是類型的一部分。[10]int[20]int是2個不同的類型。go數組是值類型。賦值或傳入函數參數時會發生值拷貝。如果需要指針類型數組,一般使用切片。

切片

切片是基于數組的方便使用對象,它具有底層數組引用和當前數組長度以及數組最大長度。切片本身是值對象,但是賦值后,兩個切片會指向同一段底層數組,故此能夠傳遞修改。數組支持范圍訪問n, err := f.Read(buf[0:32])。切片增加元素建議使用內置方法append,它支持自動擴容。

二維數組和切片

type Transform [3][3]float64
type LinesOfText [][]byte
二維切片中的每個切片長度可以不同。分配二維切片有兩種方式,如果切片們的長度不同,應該為每個切片單獨分配。如果它們的長度一樣,可以只分配一次。

  • 單獨分配例子,注意每個slice都有獨立的make分配
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
    picture[i] = make([]uint8, XSize)
}
  • 分配一次例子,注意底層切片只分配一次,然后把對應段賦給每個切片。
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

映射

鍵值對映射是一種很有用的類型。鍵可以是任何定義了equality操作的類型,注意切片不支持equality。映射也是持有底層數據結構引用,能夠傳遞修改。映射支持復合文字構造,

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

獲取不存在的鍵值,會返回值類型的0值。如果要明確知道是否存在,使用多返回

var seconds int
var ok bool
seconds, ok = timeZone[tz]

如果只想判斷鍵值是否存在,采用空標記符__, present := timeZone[tz]
刪除鍵值采用內置函數delete,它確保鍵不存在也能工作。

格式化打印

fmt包提供一系列格式化打印的方法,例如fmt.Printf, fmt.Fprintf, fmt.Sprintf,以及默認格式版本,fmt.Print, fmt.Fprint, fmt.Sprint,默認格式會在每個參數前后插入一個空格。

  • %d 顯示數字
  • %v 顯示所有類型的值
  • %+v 對于結構體顯示字段名
  • %#v 顯示對象全部信息
  • %q 顯示字符串或字節數組,也可用于數字或rune,顯示單引號標記的rune字符
  • %#q 盡可能使用反引號
  • %x 用于字符串、字節數組或數字,顯示十六進制值
  • % x 在顯示字節前后增加空格
  • %T 顯示類型
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

結果
18446744073709551615 ffffffffffffffff; -1 -1

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

結果
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

結果
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

fmt.Printf("%T\n", timeZone)

結果
map[string] int

自定義顯示方法只需重寫類型的String() string方法

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

結果
7/-2.35/"abc\tdef"

重寫方法時避免無限重入

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

應改為

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

不定參數...

  • 作為參數傳入
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}
  • 作為slice使用
func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

Append

append內置函數簽名func append(slice []T, elements ...T) []TT表示任何類型。切片通過...方式轉換為可變參數。

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

9. 初始化[9]

go的初始化比c和c++更強大,可以構造復雜結構體,不同包之間的初始化順序也會被正確處理。

常量

go常量在編譯時生成,只能是數字、字符、字符串或布爾類型。定義支持常量表達式,例如1<<3math.Sin(math.Pi/4)不支持,因為math.Sin是運行時函數。
iota枚舉器用來定義枚舉常量

type ByteSize float64
const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)
func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

ByteSize(1e13)顯示結果為9.09TB

變量

變量在運行時完成初始化

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

init函數

每個原文件可以定義init函數(支持多個init函數)。init函數會在所有變量初始化完成、所有導入包初始化完成后運行。init函數除了進行初始化外,也常做一些狀態驗證和修復。

10. 方法[10]

指針 vs 值

任何命名類型都可以定義方法(除了指針和接口)。方法定義需要接收者,它可以是指針也可以是值。值將傳遞拷貝,方法內做的修改無法影響傳入值。指針可以將修改帶出方法外。
值接收者的方法可以在值和指針上執行,指針接收者的方法只能在指針上執行。一個特例是,如果值能夠轉為指針,值上調用指針接收者方法會自動轉換為指針再調用。例如,b是值變量且可以取地址,b.Write會被自動重寫為(&b).Write

11. 接口和其它類型[11]

接口

和其它語言一樣,go的接口是定義對象行為。只要具有接口方法,就可以當接口使用。go的接口一般只有1、2個方法,名字也來源于方法。
一個類型可以實現多個接口,只要它含有指定接口的方法。

轉型

T(value)將一個類型值轉換成另一個指定類型T,如果兩個類型完全一樣,此過程并不會產生新值。(int轉為float,會產生新值)。

接口轉型和類型斷言

Type Switch已涉及接口轉型,每個case都會轉換成對應類型。如果已知接口類型,就要使用類型斷言。value.(typeName)。typeName是具體的類型名字,例如str := value.(string)。但如果轉型失敗,會發生運行時錯誤。可用下面方式避免錯誤。

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果轉型失敗,str仍然存在,只是0值。

概述

如果一個類型只是實現了一個接口,并不需要暴露這個類型而應暴露它實現的接口,以隱藏具體的實現細節。這要求構造器返回接口而不是具體實現類型。例如crc32.NewIEEEadler32.New都返回接口hash.Hash32。替換crc32算法為adler32算法,只需要修改構造器調用,而其它代碼都保持不變。

接口和方法

只要實現了接口方法的類型就實現了接口,由于幾乎所有的類型都可定義方法,故此幾乎所有的類型都可以實現接口。
例如Handler接口

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

下面的結構實現了此接口

type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

也可以用整數類型實現這個接口

type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

也可以是其它類型,比如管道

type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

函數類型可以實現接口,例如

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

http.Handle("/args", http.HandlerFunc(ArgServer))

上面的代碼實現了訪問/args時,返回系統參數。

12. 空白標識符[12]

空白標識符類似unix中的 /dev/null文件,是一個占位符但不關心它的值。

在多賦值中使用

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

未使用導入和變量

package main

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

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}

按照約定,這些空白標記符語句必須緊隨導入塊之后,并且需要提供相應的注釋信息,以便將來很容易找到并清除它們。

副作用導入

import _ "net/http/pprof"

導入這個包只是為了運行它的init函數

接口檢查

對于運行時接口檢查,如果不關心轉換值,采用空白標識符忽略轉換結果

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

有種罕見情況,需要在編譯時確認一個代碼中未使用到的接口檢查,采用空白標識符忽略轉換值。
var _ json.Marshaler = (*RawMessage)(nil)

13. 內嵌[13]

go不支持標準面向對象的繼承。go推薦使用組合,并提供內嵌達到類似繼承的效果。接口和結構都可使用內嵌。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

接口只能內嵌接口。

type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

type Job struct {
    Command string
    *log.Logger
}

內嵌是在外部類型中定義了一個類型同名字段,并可從外部類型中直接訪問內嵌數據。內嵌不支持重載和多態。go通過接口實現多態。需要多態的方法都定義為小接口(go推薦小接口),用組合代替繼承。

14. 并發[14]

不同的并發方式

go沒有采用資源競爭方式實現并發,而通過共享資源確保每個線程訪問各自的資源。總結為一句話
勿以共享內存實現通訊,而以通訊實現共享內存
go的并發方式源于CSP模型(Communicating Sequential Processes)。

go協程

在函數或方法調用前加上go就會啟動一個go協程,不同的協程能夠并發的運行在同一個代碼地址上。協程非常輕量,比分配棧大不了多少。協程可以多路復用操作系統線程,它屏蔽了線程創建、管理等的復雜細節。

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

go函數支持閉包。

通道

通道由make分配,類似隊列類型。make通道是可指定緩存大小,默認為0。通道接收方一直被擁塞直到有數據收到。對于發送方,如果通道中的數據小于緩存,只擁塞到數據復制進通道,如果通道已滿(或緩存為0),會一直擁塞到直到有數據被接收。
緩存通道可用作信號量,緩存數量就是控制的并發數量。見下例

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Don't wait for handle to finish.
    }
}

上面的代碼有個問題,可能創建無限個協程,但只有MaxOutstanding個運行。改進一下,把創建協程也放入信號量控制中。

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

注意上面代碼采用函數閉包將公共變量req的值固定在每個函數調用中。
也可用聲明新的局部變量方式完成

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

函數式編程中,更推薦閉包方式。
還有另一個方法,開啟指定數量個處理協程,同時處理。這種方案更自然。

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

通道類型的通道

通道可以傳遞任何類型,也包括通道本身。下面例子實現一個簡單的RPC。

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

客戶端代碼

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

服務端代碼

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

并行

另個應用是將一個復雜計算分散到多個CPU同時運行。見下例

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}

const numCPU = runtime.GOMAXPROCS(0) // number of CPU cores runtime.NumCPU()

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

務必理解并發和并行的區別。并發是指程序能夠獨立執行各個模塊。并行是指在多個CPU上同時執行運算以提高效率。go是一個并發語言,并不是并行語言,有些并行問題go并不適合。

簡單垃圾回收例子

go的并發設計也簡化一些非并發問題的解決。如下面這個源于RPC框架的簡單垃圾回收例子。

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}

func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

select語句如果找不到滿足子句,會執行default子句,這意味著它是非擁塞的。

15. 錯誤處理[15]

包開發者應該提供豐富的錯誤信息包含全部的錯誤信息,比如包名、操作名等。也可以使用類型斷言轉換為某種指定錯誤,進一步處理。

運行時錯誤

常見的錯誤處理方式是返回它。由調用者判斷如何處理。但如果發現一個嚴重錯誤無法處理或繞過,調用內置函數panic產生一個運行時錯誤,panic接收一個任何類型的參數,一般是表示錯誤信息的字符串。

錯誤恢復

當運行時錯誤發生時,無論是顯示產生的,還是隱式產生的例如數組下標越界,都會立刻停止當前執行并開始層層退出調用棧,退出前會執行對應的defer函數。可以使用recover方法重新截獲運行時錯誤。recover只能在defer函數中使用,因為退棧時只有defer函數能夠執行。例子如下

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

recover只有在defer函數中才可能返回非nil,defer函數中的調用不受panic和recover的影響。錯誤恢復也用來處理內部錯誤,下面的例子是regexp包處理解析錯誤。

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}
doParse代碼

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

一個需要遵守的原則是,內部運行時錯誤被轉成error返回,不要暴露到包外。
使用re-panic重新拋出運行時錯誤,錯誤棧中會包含新舊錯誤信息。


  1. . ?

  2. . ?

  3. . ?

  4. . ?

  5. . ?

  6. . ?

  7. . ?

  8. . ?

  9. . ?

  10. . ?

  11. . ?

  12. . ?

  13. . ?

  14. . ?

  15. . ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.安裝 https://studygolang.com/dl 2.使用vscode編輯器安裝go插件 3.go語...
    go含羞草閱讀 1,573評論 0 6
  • 環境搭建 Golang在Mac OS上的環境配置 使用Visual Studio Code輔助Go源碼編寫 VS ...
    隕石墜滅閱讀 5,792評論 0 5
  • 原文https://milapneupane.com.np/2019/07/06/learning-golang-...
    Gundy_閱讀 442評論 0 2
  • 寫在前面 本文是Go語言的快速入門教程,適合于具有一定C語言或者Java語言基礎的開發人員,如果您是一位Go...
    foundwei閱讀 1,870評論 5 17
  • 從上周五父親住院已經進入第六天。 今天下午我跟單位同事說一聲,去醫院陪護。輝寶讓我在家睡到三四點再去,兩點打來電話...
    平常心_9886閱讀 126評論 0 1