go源碼解析-Println的故事

本文主要經過日常經常使用的go的一個函數,深刻源碼,瞭解其底層究竟是如何實現的。html

Println

Println函數接受參數a,其類型爲…interface{}。用過Java的對這個應該比較熟悉,Java中也有…的用法。其做用是傳入可變的參數,而interface{}相似於Java中的Object,表明任何類型。java

因此,…interface{}轉換成Java的概念,就是Object args ...git

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

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

而在此處的…放在了參數的後面。咱們知道...interface{}是表明可變參數,即函數可接收任意數量的參數,並且參數參數分開寫的。web

當咱們再調用這個函數的時候,咱們就沒有必要再將參數一個一個傳給被調用函數了,直接使用a…就能夠達到相同的效果。chrome

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函數沒有獲取到資源可是p.New函數能夠申請到新的資源,就直接返回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,就沒法再執行後續的操做了。

寫在最後

看源碼是個技術活,其實這篇博客也算是一種嘗試。最近看到一個圖頗有意思,跟你們分享一下。這張圖講的是你覺得的看源碼。

而後是實際上的你看源碼。

這張圖特別形象。當你打算看一個開源項目的源碼的時候,每每像一個餓了不少天沒吃飯的人看到一桌美食同樣,巴不得幾分鐘就把桌上的東西所有吃完,最後撐的半死,所有吐了出來;又或許像上面兩張圖裏的水同樣,接的太快,最後杯子裏剩的反而越少。

相反,若是咱們慢慢的品味美食,慢慢的去接水,肚子裏的食物和水杯的水就必定會慢慢增長,直到適量爲止。

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

參考:

往期文章:

相關:

  • 微信公衆號: SH的全棧筆記(或直接在添加公衆號界面搜索微信號LunhaoHu)
相關文章
相關標籤/搜索