go源码解析Println的故事

56次阅读

共计 5742 个字符,预计需要花费 15 分钟才能阅读完成。

本文主要通过平常常用的 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,就无法再执行后续的操作了。

写在最后

看源码是个技术活,其实这篇博客也算是一种尝试。最近看到一个图很有意思,跟大家分享一下。这张图讲的是你以为的看源码。

然后是实际上的你看源码。

这张图特别形象。当你打算看一个开源项目的源码的时候,往往像一个饿了很多天没吃饭的人看到一桌美食一样,恨不得几分钟就把桌上的东西全部吃完,最后撑的半死,全部吐了出来;又或许像上面两张图里的水一样,接的太快,最后杯子里剩的反而越少。

相反,如果我们慢慢的品味美食,慢慢的去接水,肚子里的食物和水杯的水就一定会慢慢增加,直到适量为止。

我认为看源码,不应该一口吃成胖子,细水长流。从某一个小功能开始,慢慢的展开,这样才能了解到更多的东西。

参考:

  • Golang 源码剖析:fmt 标准库 — Print* 是怎么样输出的?
  • Go 语言字符类型(byte 和 rune)
  • rune 和 [] byte 区别](https://learnku.com/articles/…

往期文章:

  • 什么?你竟然还没有用这几个 chrome 插件?
  • 手把手教你从零开始搭建 SpringBoot 后端项目框架
  • 用 go-module 作为包管理器搭建 go 的 web 服务器
  • WebAssembly 完全入门——了解 wasm 的前世今身
  • 小强开饭店 - 从单体应用到微服务

相关:

  • 微信公众号:SH 的全栈笔记(或直接在添加公众号界面搜索微信号 LunhaoHu)

微信公众号.jpg

正文完
 0