携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。
在上一篇文章中,我们将 doPrintf 的宽度和精度处理部分补充完整,分析了第四层函数 printArg 的全部执行过程,并介绍了 fmt 中的导出接口方法的调用位置。本篇文章,我们继续分析 handleMethods printValue 以及以 fmt 开头的函数们。
备注:本系列文章使用的是 go 1.19 源码:
结果注释中的 · 代表一个空格
第四层函数
以fmt开头的函数们
这些格式化函数真正决定了,对于每一种类型值来说,哪些格式化动词是可用的。
我们可以整理一下这部分内容:
-
Bool:t,v
-
Integer:v,#v,d,b,o,O,x,X,c,q,U
- #v 打印含
0x前缀的十六进制表示,仅对无符号整型有效
- #v 打印含
-
Float:v,b,g,G,x,X,f,e,E,F
- v 和 g 的效果相同
-
Complex:v,b,g,G,x,X,f,F,e,E
-
实部与虚部的最大位数相同
-
虚部总是带有符号,所以在格式化虚部之前要设置
p.fmt.plus,以免被p.fmt.space消掉
-
-
String:v,#v,s,x,X,q
- 其中 #v 和 q 的效果相同,v 和 s 的效果相同
-
Bytes:v,d,#v,s,x,X,q
-
#v 打印 nil 是
[]byte(nil),打印非 nil 值是[]byte{0x49, 0x20} -
v 和 d 打印 nil 是
[],打印非 nil 值是[73 32] -
默认情况是用
printValue打印反射值,与其他 fmt 函数不同(它们都是用badVerb输出错误)
-
fmtPointer 稍微特殊一些,把它单拿出来说:
首先,它要检查值的类型必须是信道、函数、映射、指针、切片中的一种,然后再判断格式化动词:
-
v
- 打印以
0x开头的十六进制数
- 打印以
-
#v
- 打印格式如
(*int)(0xc0000120a8)
- 打印格式如
-
p
- p 和 v 效果相同
-
#p
- 不带
0x的十六进制数
- 不带
-
b,o,d,x,X
handleMethods
对于非Go中的内置的基本类型,会通过本函数来查找它的格式化方法,包括 String, Gostring, Format 和 Error。
if p.erroring {
return
}
if verb == 'w' {
err, ok := p.arg.(error)
if !ok || !p.wrapErrs || p.wrappedErr != nil {
p.wrappedErr = nil
p.wrapErrs = false
p.badVerb(verb)
return true
}
p.wrappedErr = err
verb = 'v'
}
首先,当打印错误时,即在执行 baVerb 的过程中,不使用本函数。
其次,只有当调用 Errorf 函数打印 error 值时,才可以使用格式化动词 w,且在函数中只能出现一次。
下面依次查找四种格式化接口的实现,其代码逻辑相同:
handled = true
defer p.catchPanic(...)
p.fmtString(...)
return
catchPanic
本函数会恢复可能出现的恐慌。
对于空指针会输出 <nil> ,对于其他值,错误信息。
为了防止在打印错误时又出现错误,而形成无线递归,使用了下面的语句:
if p.panicking {
panic(err)
}
在打印错误前会将 p.panicking 设为 true,打印结束后再设回 false。
printValue
下面,让我们来看看最后一个函数。
实际上,这个函数和 printArg 差不多,只是它处理的是反射值。
if depth > 0 && value.IsValid() && value.CanInterface() {
p.arg = value.Interface()
if p.handleMethods(verb) {
return
}
}
首先,对于 printArg 没有处理过的值(处理过 depth 会变为 0),尝试调用它们的特殊方法。
接下来,对反射值进行类型选择,如果是基本类型,依然是调用 fmt 开头的函数去格式化。
如果是映射,会对映射的键按从小到大的顺序排序,然后打印。
var a = map[string]int{"john": 2, "aaron": 1, "ella": 0}
Printf("%#v\n", a) // map[aaron:1 ella:0 john:2]
Printf("%#v\n", a) // map[string]int{"aaron":1, "ella":0, "john":2}
如果是结构体,会将字段名和字段值打印出来,根据不同的格式符,有不同的输出格式。
type person struct {
name string
age int
}
p := person{"steve", 20}
Printf("%v", p) // {steve 20}
Printf("%+v", p) // {name:steve age:20}
Printf("%#v", p) // main.person{name:"steve", age:20}
如果是接口,会先获取接口值,然后再通过 printValue 去打印。
如果是数组或切片,先处理 Uint8 类型加上 s,q,x,X 这些特殊的格式化动词,可以直接获取值并写入;对于其他的类型,则需要通过循环 printValue 去打印每一个值。
如果是指针,且指向的类型是数组、切片、映射、结构体,则打印一个 &,在调用 printValue 并且深度加一。
如果是信道、函数、不安全指针或指向它们的指针,通过 fmtPointer 去处理。
最后,通过 unknownType 处理未知类型。
总结
在本篇文章中,我们分析了以fmt开头的函数们,还有 handleMethods 和 printValue。至此,我们已经将 print.go 的所有执行逻辑都分析完了,fmt 的半壁江山已经拿下。下一篇文章,我们会开始 scan.go 的分析。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿