从源码学习 Go 标准库(一):fmt - print(4)

184 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

前言

本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。

第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。

上一篇文章中,我们将 doPrintf 的宽度和精度处理部分补充完整,分析了第四层函数 printArg 的全部执行过程,并介绍了 fmt 中的导出接口方法的调用位置。本篇文章,我们继续分析 handleMethods printValue 以及以 fmt 开头的函数们。

备注:本系列文章使用的是 go 1.19 源码:

github.com/golang/go/t…

结果注释中的 · 代表一个空格

第四层函数

以fmt开头的函数们

github.com/golang/go/b…

这些格式化函数真正决定了,对于每一种类型值来说,哪些格式化动词是可用的。

我们可以整理一下这部分内容:

  • Bool:t,v

  • Integer:v,#v,d,b,o,O,x,X,c,q,U

    • #v 打印含 0x 前缀的十六进制表示,仅对无符号整型有效
  • 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

github.com/golang/go/b…

对于非Go中的内置的基本类型,会通过本函数来查找它的格式化方法,包括 StringGostringFormatError

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

github.com/golang/go/b…

本函数会恢复可能出现的恐慌。

对于空指针会输出 <nil> ,对于其他值,错误信息。

为了防止在打印错误时又出现错误,而形成无线递归,使用了下面的语句:

if p.panicking {
        panic(err)
}

在打印错误前会将 p.panicking 设为 true,打印结束后再设回 false

printValue

github.com/golang/go/b…

下面,让我们来看看最后一个函数。

实际上,这个函数和 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开头的函数们,还有 handleMethodsprintValue。至此,我们已经将 print.go 的所有执行逻辑都分析完了,fmt 的半壁江山已经拿下。下一篇文章,我们会开始 scan.go 的分析。

最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿