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

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

前言

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

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

本篇文章将进入到 print.go 的学习,我们将进一步认识 fmt 包是如何进行打印内容的格式化工作的,并且通过对各个接口实现的深入理解,我们可以更清楚地知道如何实现自己的接口方法。

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

github.com/golang/go/t…

print

类型和常量

github.com/golang/go/b…

常量里面定义了一些固定的打印内容,比如切片或字段间的分隔符,不同类型的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 中一共定义了四个接口,分别是 StateFormatterStringerGostringer ,我们挨个来看看它们是干什么的。

type Stringer interface {
	String() string
}
type GoStringer interface {
	GoString() string
}
复制代码

Stringer 接口应该是我们最常用的,通过 String 方法,我们可以给声明的类型添加默认的打印格式,对应 %v 格式符。 GostringerStringer 类似,它控制值的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 中的类型和常量以及它们的用途,并介绍了打印机的对象重用机制。在下一篇中,我们将梳理打印函数的执行流程,并进行更详细地分析。

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

猜你喜欢

转载自juejin.im/post/7132504150083895327
今日推荐