go源码解析-Println的故事
本文主要通过平常常用的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函数没有获取到资源但是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,就无法再执行后续的操作了。
写在最后
看源码是个技术活,其实这篇博客也算是一种尝试。最近看到一个图很有意思,跟大家分享一下。这张图讲的是你以为的看源码。

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

这张图特别形象。当你打算看一个开源项目的源码的时候,往往像一个饿了很多天没吃饭的人看到一桌美食一样,恨不得几分钟就把桌上的东西全部吃完,最后撑的半死,全部吐了出来;又或许像上面两张图里的水一样,接的太快,最后杯子里剩的反而越少。
相反,如果我们慢慢的品味美食,慢慢的去接水,肚子里的食物和水杯的水就一定会慢慢增加,直到适量为止。
我认为看源码,不应该一口吃成胖子,细水长流。从某一个小功能开始,慢慢的展开,这样才能了解到更多的东西。
参考:
往期文章:
- 什么?你竟然还没有用这几个chrome插件?
- 手把手教你从零开始搭建SpringBoot后端项目框架
- 用go-module作为包管理器搭建go的web服务器
- WebAssembly完全入门——了解wasm的前世今身
- 小强开饭店-从单体应用到微服务
相关:
- 微信公众号: SH的全栈笔记(或直接在添加公众号界面搜索微信号LunhaoHu)
go源码解析-Println的故事的更多相关文章
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- HashMap 源码解析
HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表. ...
- 小学徒成长系列—StringBuilder & StringBuffer关键源码解析
在前面的博文<小学徒成长系列—String关键源码解析>和<小学徒进阶系列—JVM对String的处理>中,我们讲到了关于String的常用方法以及JVM对字符串常量Strin ...
- EventBus3.0源码解析
本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...
- Java集合---Array类源码解析
Java集合---Array类源码解析 ---转自:牛奶.不加糖 一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Prim ...
- Java 集合系列13之 WeakHashMap详细介绍(源码解析)和使用示例
概要 这一章,我们对WeakHashMap进行学习.我们先对WeakHashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用WeakHashMap.第1部分 WeakHashMap介绍 ...
- Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例
概要 前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...
- Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例
概要 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解Arra ...
- Java 集合系列06之 Vector详细介绍(源码解析)和使用示例
概要 学完ArrayList和LinkedList之后,我们接着学习Vector.学习方式还是和之前一样,先对Vector有个整体认识,然后再学习它的源码:最后再通过实例来学会使用它.第1部分 Vec ...
随机推荐
- 微信团队分享:极致优化,iOS版微信编译速度3倍提升的实践总结
1.引言 岁月真是个养猪场,这几年,人胖了,微信代码也翻了. 记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟.如今用公司配的 17 年款 27-inch iMac 编译要接近半小时:偶 ...
- 在Linux系统下制作系统启动盘(Ubuntu Linux)
在Linux系统下制作系统启动盘有两种方法: 1.用dd命令 2.用Linux自带的图形界面工具 Startup Disk Creator 本教程使用第2种方式,用Linux自带的图形界面工具制作系统 ...
- linux,centos,php,word转图片方法
刚开始的时候是客户的活儿,真的是没有想到,被这样一个方法给卡住了,真是醉了,经过漫长的一周,终于搞定了这个问题,以下就来给大家介绍以下这个方法把.能方便大家. (直接转换没办法,所以找了个折中的办法, ...
- chrome显示正常,IE报400的错
发现是因为参数里面有一个是中文,应该是IE没有转码,所以会报错,只要用encodeURI()实现转码即可
- Pycharm 解释器的快捷键
Ctrl+shift+Z 反撤销 Ctrl +/ 注释 ctrl+d 复制粘贴选中 Ctrl+y 删除默认一行 Ctrl+shift+r 全局搜索 Ctrl+alt+/ 代码整理 compare w ...
- Linux Bash文本操作之grep篇
Linux grep命令用于查找文件里符合条件的字符串.是文本检索中常用的工具之一. grep 指令在文件中查找能够匹配指定模式字符串的行.如果没有指定文件名,或者文件名为 - ,则从标准输入设 ...
- cuckoo沙箱技术分析全景图
从事信息安全技术行业的小伙伴们都知道沙箱技术(有些也称沙盒),用来判断一个程序或者文件是否是恶意的病毒.木马.漏洞攻击exp或其他恶意软件.其原理简单来说就是提供了一个虚拟的环境,把分析目标放到这个虚 ...
- git项目创建及在idea工具中使用
1.安装git管理工具 2.在自己github账号上创建一个项目仓库,比如我创建的是renrenView 网页翻译如下: 参数解析如下: 3.本地项目同步到远程仓库步骤 在本地初始化git项目 git ...
- JavaFX如何为按钮设置快捷键?
JavaFX为按钮设置快捷键的方式有很多,先说下常见的一种. 第一种: KeyCodeCombination kc1 = new KeyCodeCombination(KeyCode.W, KeyCo ...
- Python—执行系统命令的四种方法
一.os.system方法 这个方法是直接调用标准C的system() 函数,仅仅在一个子终端运行系统命令,而不能获取命令执行后的返回信息. os.system(cmd)的返回值.如果执行成功,那么会 ...
