携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt
包,它包括 format
print
scan
errors
这四个部分,我们将按照这个顺序来依次分析。
本篇文章将进入到 print.go
的学习,我们将进一步认识 fmt
包是如何进行打印内容的格式化工作的,并且通过对各个接口实现的深入理解,我们可以更清楚地知道如何实现自己的接口方法。
备注:本系列文章使用的是 go 1.19 源码:
类型和常量
常量里面定义了一些固定的打印内容,比如切片或字段间的分隔符,不同类型的nil值,以及一些错误值。
b := []byte{67, 68, 69, 70, 71}
Printf("%#v\n", b) // []byte{0x43, 0x44, 0x45, 0x46, 0x47}
Printf("% ", 2.3) // %!(NOVERB)%!(EXTRA float64=2.3)
Printf("%z", 2.3) // %!z(float64=2.3)
Printf("%v", nil) // <nil>
复制代码
相比于使用字节数组写入,使用这些字符串常量的开销更小。
print.go
中一共定义了四个接口,分别是 State
、 Formatter
、 Stringer
和 Gostringer
,我们挨个来看看它们是干什么的。
type Stringer interface {
String() string
}
type GoStringer interface {
GoString() string
}
复制代码
Stringer
接口应该是我们最常用的,通过 String
方法,我们可以给声明的类型添加默认的打印格式,对应 %v
格式符。 Gostringer
和 Stringer
类似,它控制值的Go语法表示,对应 %#v
格式符。
type State interface {
Write(b []byte) (n int, err error)
Width() (wid int, ok bool)
Precision() (prec int, ok bool)
Flag(c int) bool
}
复制代码
State
表示传递给自定义格式化程序的打印机状态,它提供 io
的写入操作以及格式说明符的详细信息,包括:
-
Write
用来将格式化后的打印内容输出 -
Width
返回宽度值以及是否设置了宽度 -
Precision
返回精度值以及是否设置了精度 -
Flag
判断是否设置了某个标识符
type Formatter interface {
Format(f State, verb rune)
}
复制代码
Formatter
是格式化程序,用于解释打印机状态和格式化说明符,它可以用来处理反射值对象和其它非简单类型,fmt
包会传给我们打印机 p
和格式符,我们可以自定义处理方法并将结果写入到 p.buf
中。
// func (p *pp) handleMethods(verb rune) (handled bool)
if formatter, ok := p.arg.(Formatter); ok {
handled = true
defer p.catchPanic(p.arg, verb, "Format")
formatter.Format(p, verb)
return
}
复制代码
那么下面,我们来看看 fmt
包中实现的打印机状态(State
):
type buffer []byte
type pp struct {
buf buffer
arg any // 将当前值作为接口保存
value reflect.Value // 当前值为反射值时,用 value 代替 arg
fmt fmt
reordered bool // 记录是否对格式化字符串使用了重新排序
goodArgNum bool // 记录最近的重新排序指令是否有效
panicking bool // 用于避免恐慌和恢复的无线递归
erroring bool // 在打印错误字符串时,避免调用 handleMethods
wrapErrs bool // 包含 %w 时设置
wrappedErr error // 记录 %w 的目标
}
复制代码
以及 pp
用来实现 State
接口的四个方法:
func (p *pp) Width() (wid int, ok bool) { return p.fmt.wid, p.fmt.widPresent }
func (p *pp) Precision() (prec int, ok bool) { return p.fmt.prec, p.fmt.precPresent }
func (p *pp) Flag(b int) bool {
switch b {
case '-':
return p.fmt.minus
case '+':
return p.fmt.plus || p.fmt.plusV
case '#':
return p.fmt.sharp || p.fmt.sharpV
case ' ':
return p.fmt.space
case '0':
return p.fmt.zero
}
return false
}
func (p *pp) Write(b []byte) (ret int, err error) {
p.buf.write(b)
return len(b), nil
}
复制代码
fmt
类型是在上一篇文章所讲的 format.go
中的定义的。
pp
还有很多其他的方法,用来控制整个格式化和打印的流程,我们在下一篇中详细介绍。
最后再补充一点关于 pp
的创建和释放的内容。pp
采用了对象重用机制,利用 sync.Pool
来缓存已分配但未使用的 pp
,以避免每次调用都要进行一次分配,降低分配的开销。
var ppFree = sync.Pool{
New: func() any { return new(pp) },
}
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
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)
}
复制代码
创建打印机时,会尝试从缓存池中抓取一个 pp
或者分配一个新的 pp
结构;释放时,会将 pp
放回缓存池中。
总结
在本篇文章中,我们学习了 print.go
中的类型和常量以及它们的用途,并介绍了打印机的对象重用机制。在下一篇中,我们将梳理打印函数的执行流程,并进行更详细地分析。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿