go原始碼解析-Println的故事

detectiveHLH發表於2019-06-14

本文主要通過平常常用的go的一個函式,深入原始碼,瞭解其底層到底是如何實現的。

Println

Println函式接受引數a,其型別為…interface{}。用過Java的對這個應該比較熟悉,Java中也有…的用法。其作用是傳入可變的引數,而interface{}類似於Java中的Object,代表任何型別。

所以,…interface{}轉換成Java的概念,就是Object args ...

Println函式中沒有什麼實現,只是return了Fprintln函式。

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
} 

而在此處的…放在了引數的後面。我們知道...interface{}是代表可變引數,即函式可接收任意數量的引數,而且引數引數分開寫的。

當我們再呼叫這個函式的時候,我們就沒有必要再將引數一個一個傳給被呼叫函式了,直接使用a…就可以達到相同的效果。

Fprintln

該函式接收引數os.Stdout.write,和需要列印的資料作為引數。

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

sync.Pool

從廣義上看,newPrinter申請了一個臨時物件池。我們逐行來看newPrinter函式做了什麼。

var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

sync.Pool是go的臨時物件池,用於儲存被分配了但是沒有被使用,但是未來可能會使用的值。以此來減少 GC的壓力。

ppFree.Get

ppFree.Get()上有大量的註釋。

Get selects an arbitrary item from the Pool, removes it from the Pool, and returns it to the caller.

Get may choose to ignore the pool and treat it as empty. Callers should not assume any relation between values passed to Put and the values returned by Get.

If Get would otherwise return nil and p.New is non-nil, Get returns the result of calling p.New.

麻瓜翻譯一波。

Get會從臨時物件池中任意選一個printer返回給呼叫者,並且將此項從物件池中移除。

Get也可以選擇把臨時物件池當成空的忽略。呼叫者不應該假設傳遞給Put方法的值和Get返回的值之間存在任何關係。

如果Get返回nil或者p,New就一定不為空。Get將返回撥用p.New的結果。

上面提到的Put方法,作用是將物件加入到臨時物件池中。

p := ppFree.Get().(*pp)下面的三個引數分別代表什麼呢?

引數名 用途
p.panicking 由catchPanic設定,是為了避免在panic和recover中無限迴圈
p.erroring 當列印錯誤的識別符號的時候,防止呼叫handleMethods
p.wrapErrs 當格式字串包含了動詞時的設定
fmt.init 初始化 fmt 配置,會設定 buf 並且清空 fmtFlags 標誌位

然後就返回這個新建的printer給呼叫方。

doPrintln

接下來是doPrintln函式。

doPrintln就跟doPrint類似,但是doPrintln總是會在引數之間新增一個空格,並且在最後一個引數後面新增換行符。以下是兩種輸出方式的對比。

fmt.Println("test", "hello", "word") // test hello word
fmt.Print("test", "hello", "word")   // testhelloword% 

看了樣例,我們再具體看一下doPrintln的具體實現。

func (p *pp) doPrintln(a []interface{}) {
    for argNum, arg := range a {
        if argNum > 0 {
            p.buf.writeByte(' ')
        }
        p.printArg(arg, 'v')
    }
    p.buf.writeByte('\n')
}

這個函式的思路很清晰。遍歷所有傳入的需要print的引數,在除了第一個 引數以外的所有引數的前面加上一個空格,寫入buffer中。然後呼叫printArg函式,再將換行符寫入buffer中。

writeByte的實現很簡單,使用了append函式,將傳入的引數,append到buffer中。

func (b *buffer) writeByte(c byte) {
    *b = append(*b, c)
}

printArg

從上可以看出,呼叫printArg函式的時候,傳入了兩個引數。

第一個是需要列印的引數,第二個則是verb,在doPrintln中我們傳的是單引號的v。那麼在go中的單引號和雙引號有什麼區別呢?下面我們通過一個表格來對比一下在不同的語言中,單引號和雙引號的區別。

語言 單引號 雙引號
Java char String
JavaScript string string
go rune String
Python string string

rune

那麼rune到底是什麼型別呢?rune是int32的別名,在任何方面等於int32相同,用於區分字串和整形。其實現很簡單,type rune = int32,rune常用來表示Unicode中的碼點,其例子如下所示。

str := "hello 你好"
fmt.Println([]rune(str)) // [104 101 108 108 111 32 20320 22909]

說到了rune就不得不說一下byte。同樣,我們通過例子來看一下byte和rune的區別。

str := "hello 你好"
fmt.Println([]rune(str)) // [104 101 108 108 111 32 20320 22909]
fmt.Println([]byte(str)) // [104 101 108 108 111 32 228 189 160 229 165 189]

沒錯,區別就在型別上。rune是type rune = int32,一個位元組;而byte是type byte = uint8,四個位元組。實際上,golang中的字串的底層是靠byte陣列實現的。如果我們處理的資料中出現了中文字元,都可用rune來處理。例如。

str := "hello 你好"
fmt.Println(len(str))         // 12
fmt.Println(len([]rune(str))) // 8

printArg具體實現

func (p *pp) printArg(arg interface{}, verb rune) {
    p.arg = arg
    p.value = reflect.Value{}

    if arg == nil {
        switch verb {
        case 'T', 'v':
            p.fmt.padString(nilAngleString)
        default:
            p.badVerb(verb)
        }
        return
    }

    switch verb {
    case 'T':
        p.fmt.fmtS(reflect.TypeOf(arg).String())
        return
    case 'p':
        p.fmtPointer(reflect.ValueOf(arg), 'p')
        return
    }

  switch f := arg.(type) {
    case bool:
        p.fmtBool(f, verb)
    case float32:
        p.fmtFloat(float64(f), 32, verb)
    case float64:
        p.fmtFloat(f, 64, verb)
    case complex64:
        p.fmtComplex(complex128(f), 64, verb)
    case complex128:
        p.fmtComplex(f, 128, verb)
    case int:
        p.fmtInteger(uint64(f), signed, verb)
    case int8:
        p.fmtInteger(uint64(f), signed, verb)
    case int16:
        p.fmtInteger(uint64(f), signed, verb)
    case int32:
        p.fmtInteger(uint64(f), signed, verb)
    case int64:
        p.fmtInteger(uint64(f), signed, verb)
    case uint:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint8:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint16:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint32:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint64:
        p.fmtInteger(f, unsigned, verb)
    case uintptr:
        p.fmtInteger(uint64(f), unsigned, verb)
    case string:
        p.fmtString(f, verb)
    case []byte:
        p.fmtBytes(f, verb, "[]byte")
    case reflect.Value:
        if f.IsValid() && f.CanInterface() {
            p.arg = f.Interface()
            if p.handleMethods(verb) {
                return
            }
        }
        p.printValue(f, verb, 0)
    default:
        if !p.handleMethods(verb) {
            p.printValue(reflect.ValueOf(f), verb, 0)
        }
    }
}

可以看到有一部分型別是通過反射獲取到的,而大部分都是switch case出來的,並不是所有的型別都用的反射,相對的提高了效率。

例如,我們傳入的是字串。則接下來就會走到fmtString。

fmtString

從printArg中帶來的引數有需要列印的字串,以及rune型別的'v'。

func (p *pp) fmtString(v string, verb rune) {
    switch verb {
    case 'v':
        if p.fmt.sharpV {
            p.fmt.fmtQ(v)
        } else {
            p.fmt.fmtS(v)
        }
    case 's':
        p.fmt.fmtS(v)
    case 'x':
        p.fmt.fmtSx(v, ldigits)
    case 'X':
        p.fmt.fmtSx(v, udigits)
    case 'q':
        p.fmt.fmtQ(v)
    default:
        p.badVerb(verb)
    }
}

p.fmt.sharpV在過程中沒有被重新賦值,初始化的零值為false。所以下一步會進入fmtS。

fmtS

func (f *fmt) fmtS(s string) {
    s = f.truncateString(s)
    f.padString(s)
}

如果存在設定的精度,則truncate將字串s截斷為指定的精度。多用於需要輸出數字時。

func (f *fmt) truncateString(s string) string {
    if f.precPresent {
        n := f.prec
        for i := range s {
            n--
            if n < 0 {
                return s[:i]
            }
        }
    }
    return s
}

而padString則將字串s寫入buffer中,最後呼叫io的包輸出就好了。

free

func (p *pp) free() {
    if cap(p.buf) > 64<<10 {
        return
    }

    p.buf = p.buf[:0]
    p.arg = nil
    p.value = reflect.Value{}
    p.wrappedErr = nil
    ppFree.Put(p)
}

在前面講過,要列印的時候,需要從臨時物件池中獲取一個物件,避免重複建立。而在此處,用完之後就需要通過Put函式將其放回臨時物件池中,已備下次呼叫。

當然,並不是無限的將用過的變數放入物件池。如果緩衝區的大小超過了設定的闕值也就是65535,就無法再執行後續的操作了。

寫在最後

看原始碼是個技術活,其實這篇部落格也算是一種嘗試。最近看到一個圖很有意思,跟大家分享一下。這張圖講的是你以為的看原始碼。

go原始碼解析-Println的故事

然後是實際上的你看原始碼。

go原始碼解析-Println的故事

這張圖特別形象。當你打算看一個開源專案的原始碼的時候,往往像一個餓了很多天沒吃飯的人看到一桌美食一樣,恨不得幾分鐘就把桌上的東西全部吃完,最後撐的半死,全部吐了出來;又或許像上面兩張圖裡的水一樣,接的太快,最後杯子裡剩的反而越少。

相反,如果我們慢慢的品味美食,慢慢的去接水,肚子裡的食物和水杯的水就一定會慢慢增加,直到適量為止。

我認為看原始碼,不應該一口吃成胖子,細水長流。從某一個小功能開始,慢慢的展開,這樣才能瞭解到更多的東西。

參考:

往期文章:

相關:

  • 微信公眾號: SH的全棧筆記(或直接在新增公眾號介面搜尋微訊號LunhaoHu)
    go原始碼解析-Println的故事

相關文章