一起读Go源码(第12期) fmt包
概述
朋友们好啊,这一期我们来看看Go源码的fmt包部分,fmt大家都很熟悉了,写第一行Go代码时打印语句用的就是fmt.Println("hello world")写的。实际上fmt包提供了强大的格式化输入输出功能,下面将结合源码从格式化输入输出,错误格式化这两方面跟大伙介绍一下fmt包。首先看一下源代码文件部分,doc里面是文档文本内容,功能实现在errors.go,format.go,print.go,scan.go这几个文件实现,剩下的就是测试代码。

格式化输入输出
格式化输入 Scan
格式化输入指的是从键盘或其他输入源读取数据,按照指定格式解析到变量中,我就不把全部示例写一遍了,挑了一些比较主流用法的,下面介绍的是最基础Scan用法,从标准输入读取,请看下面的示例:
var name stringvar age intlog.Println("请输入姓名跟年龄:")scanNum, err := fmt.Scan(&name, &age)if err != nil { log.Println(err) return}log.Println("获取数量:", scanNum, "姓名:", name, "年龄:", age)
2026/03/06 19:40:38 请输入姓名跟年龄:Dawei 182026/03/06 19:41:10 获取数量: 2 姓名: Dawei 年龄: 18
此时将程序运行起来,在控制台上输入Dawei 18,注意输入中间需要空格再回车(下面会看到为什么要空格),能看到打印结果:得到2个结果,分别把值赋给了name和age两个变量。
看看Scan的源码:注释的意思是:Scan扫描从标准输入读取的文本,将连续的空格分隔的值;它返回成功扫描的条目数量,如果该数量少于参数数量会报错返回err并告知原因。Scan函数传入any个参数,但里只有一行代码,交给Fscan处理了。我也把Fscan的源码贴在下面了,注释写的是Fscan扫描从r读到的文本,价格连续的空格分隔的值依次存储到连续的参数中(换行符当做空格)。它返回成功扫描的条目数量,err告知为啥报错。
// Scan scans text read from standard input, storing successive// space-separated values into successive arguments. Newlines count// as space. It returns the number of items successfully scanned.// If that is less than the number of arguments, err will report why.func Scan(a ...any) (n int, err error) { return Fscan(os.Stdin, a...)}// Fscan scans text read from r, storing successive space-separated// values into successive arguments. Newlines count as space. It// returns the number of items successfully scanned. If that is less// than the number of arguments, err will report why.func Fscan(r io.Reader, a ...any) (n int, err error) { s, old := newScanState(r, true, false) n, err = s.doScan(a) s.free(old) return}
在Fscan函数中:
-
1. 创建 newScanState扫描对象,里面封装了读取器、缓冲区以及当前扫描的位置信息,// ss is the internal implementation of ScanState.type ss struct { rs io.RuneScanner // where to read input buf buffer // token accumulator count int // runes consumed so far. atEOF bool // already read EOF ssave} -
2. 运行 s.doScan(a)执行扫描操作,它将遍历传入参数a,从r中读取数据并进行解析(核心操作)。// doScan does the real work for scanning without a format string.func (s *ss) doScan(a []any) (numProcessed int, err error) { defer errorHandler(&err) for _, arg := range a { s.scanOne('v', arg) numProcessed++ } // Check for newline (or EOF) if required (Scanln etc.). if s.nlIsEnd { for { r := s.getRune() if r == '\n' || r == eof { break } if !isSpace(r) { s.errorString("expected newline") break } } } return} -
3. 最后运行 s.free(old)释放或重置扫描状态对象以提高性能。
接下来介绍Scanf,功能是按指定格式读取,如下面实例,输入如2026-3-6这种格式的文本,通过Scanf就能将里面的年月日值读出来赋值给相应变量。
var year, month, day intlog.Println("请输入日期(xxxx-xx-xx):")scanf_num, err := fmt.Scanf("%d-%d-%d", &year, &month, &day)if err != nil { log.Println(err) return}log.Println("获取数量", scanf_num, "年/月/日:", year, month, day)
2026/03/06 19:23:36 请输入日期(xxxx-xx-xx):2026-3-62026/03/06 19:24:08 获取数量 3 年/月/日: 2026 3 6
至于是怎么实现的也请看下面贴的Scanf源码,注释写的是Scanf从标准输入扫描文本,根据格式将后续的空格分隔的值存储到后续参数中,它返回成功扫描的条目。如果数量小于参数个数将会报错,输入中的换行符必须与格式中的换行符匹配。跟上面类似,直接交给Fscanf,该方法从r中扫描文本,根据格式format将后续的空白分隔的值依次存储到后续的参数中,它返回成功解析的条目数。
// Scanf scans text read from standard input, storing successive// space-separated values into successive arguments as determined by// the format. It returns the number of items successfully scanned.// If that is less than the number of arguments, err will report why.// Newlines in the input must match newlines in the format.// The one exception: the verb %c always scans the next rune in the// input, even if it is a space (or tab etc.) or newline.func Scanf(format string, a ...any) (n int, err error) { return Fscanf(os.Stdin, format, a...)}
最后还有Fscanf,能够从io.Reader(如文件、网络连接、字符串等)获取文件,下面实例从一个txt文件中读取内容并打印出来,对了如果报错可能是运行目录对不上的问题,可以用wd, _ := os.Getwd()获取当前目录先确认一遍。如下面示例 ,首先调用os.Open打开文件返回file作为数据源。在调用Fscanf时指定格式,”%s %s”,最后打印成功解析到的参数。
file, err := os.Open("./fmtdemo/file.txt")if err != nil { log.Println(err) panic(err)}defer file.Close()var s1, s2 stringfmt.Fscanf(file, "%s %s", &s1, &s2)fmt.Println("从file读取到:", s1, " ", s2)// 从file读取到: 夏天夏天悄悄过去留下小秘密
Fscanf函数源码中:s, old := newScanState(r, false, false)创建扫描状态,然后n, err = s.doScanf(format, a)进行格式化扫描,最后s.free(old)释放扫描状态,跟上面Scanf一致。
// Fscanf scans text read from r, storing successive space-separated// values into successive arguments as determined by the format. It// returns the number of items successfully parsed.// Newlines in the input must match newlines in the format.func Fscanf(r io.Reader, format string, a ...any) (n int, err error) { s, old := newScanState(r, false, false) n, err = s.doScanf(format, a) s.free(old) return}
格式化输出
fmt包在格式化输出这一块主要分成三类:Print,Sprintf和Fprintf。
Print就是直接输出内容,也不换行;Println就是输出内容后自动换行;Printf则是根据格式化动词进行格式化输出,格式化动词以%开头,后面跟一个或多个字符,用于指定输出格式,具体可以参考这个网址:https://www.w3ccoo.com/go/go_formatting_verbs.html
name := "dawei"fmt.Printf("hello %v \n", name)fmt.Printf("%+v \n", name)fmt.Printf("%#v \n", name)fmt.Printf("%T \n", name)fmt.Printf("%t \n", true)fmt.Printf("保留2位小数: %.2f, 科学计数法: %e\n", 1.2345, 100000.0)/*hello dawei dawei "dawei" string true 保留2位小数: 1.23, 科学计数法: 1.000000e+05*/
Spritnf会格式化返回字符串,不会输出到控制台。Fprintf会格式化写入到已实现的io.Writer接口对象(文件、网络连接等)。接下来我们分别看看Printf,Sprintf和Fprintf的源码:
-
• Printf:根据格式说明符进行格式化并写入标准输出,它返回写入的字节数和遇到的任何写入错误。内部直接调用 Fprintf。 -
• Sprintf:根据格式说明符进行格式化并返回结果字符串。先从 newPrinter()获取print对象,该对象包含一个字节缓冲区buf和其他信息。p.doPrint(format,a)将格式化的内容写入p.buf,再将缓冲区的内容转换为字符串,最后将p.free()对象归还到池中以后可以复用。 -
• Fprintf :根据格式说明符进行格式化并写入 w,它返回写入的字节数和遇到的任何写入错误。也是从 newPrinter()中获取print对象,格式化内容写入p.buf,将缓冲区内容写入目标w,最后p.free()归还printer对象。
// Printf formats according to a format specifier and writes to standard output.// It returns the number of bytes written and any write error encountered.func Printf(format string, a ...any) (n int, err error) { return Fprintf(os.Stdout, format, a...)}// Sprintf formats according to a format specifier and returns the resulting string.func Sprintf(format string, a ...any) string { p := newPrinter() p.doPrintf(format, a) s := string(p.buf) p.free() return s}// Fprintf formats according to a format specifier and writes to w.// It returns the number of bytes written and any write error encountered.func Fprintf(w io.Writer, format string, a ...any) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return}
错误格式化
fmt包errorf的主要作用是返回错误,提供统一的方法生成包含具体错误信息的error。用法包括基础的格式化错误生成简单错误以及携带上下文信息。
code := 101err := fmt.Errorf("error: %d", code)fmt.Println(err)err2 := AddMsg()fmt.Println(err2)...func AddMsg() error { return fmt.Errorf("error: %d", 102)}//error: 101//error: 102
更多时候的用法是加入到错误链——错误链指的是将错误内部包含另一个错误,形成一种链式结构。每层错误都添加了新的上下文信息,同时保留内的原始错误,这样就能更好定位错误了;在Go1.13后引用了%w动词和errors.Is和errors.As函数。下面就来看看例子:在addMsg函数直接返回错误,在fmt.Errorf里%w将错误包装起来后返回。在主函数那运行时errors.Is(err,os.ErrNotExist)沿着错误链是否有错误等于os.ErrNotExist判断原因;在下面的errors.As(err,&pathErr)在错误链中查找第一个类型为*os.PathError的错误并赋值到pathErr,这样就能读到Path,Op等信息了
err := addMsg() if err != nil { fmt.Println(err) if errors.Is(err, os.ErrNotExist) { fmt.Println("Error", err.Error()) } var pathErr *os.PathError if errors.As(err, &pathErr) { fmt.Printf("操作:%s, 路径:%s, 底层错误=%v\n", pathErr.Op, pathErr.Path, pathErr.Err) } }}func addMsg() error { _, err := os.Open("abc.txt") if err != nil { return fmt.Errorf("open abc.txt: %w", err) } return nil}/*open abc.txt: open abc.txt: no such file or directoryError open abc.txt: open abc.txt: no such file or directory操作:open, 路径:abc.txt, 底层错误=no such file or directory*/
最后看一眼Errorf源码,Errorf 根据格式说明符进行格式化,并返回一个满足 error 接口的字符串;如果格式说明符中包含一个带有 error 类型操作数的%w动词则返回的错误将实现一个Unwrap方法,该方法返回该操作数。如果存在多个 %w 动词,返回的错误将实现一个Unwrap方法。返回一个 []error,其中包含所有 %w 操作数,顺序与它们在参数中出现的顺序一致,将 %w 动词用于未实现 error 接口的操作数是无效的。除此之外,%w 动词是 %v 的同义词。
// Errorf formats according to a format specifier and returns the string as a// value that satisfies error.//// If the format specifier includes a %w verb with an error operand,// the returned error will implement an Unwrap method returning the operand.// If there is more than one %w verb, the returned error will implement an// Unwrap method returning a []error containing all the %w operands in the// order they appear in the arguments.// It is invalid to supply the %w verb with an operand that does not implement// the error interface. The %w verb is otherwise a synonym for %v.func Errorf(format string, a ...any) error { p := newPrinter() p.wrapErrs = true p.doPrintf(format, a) s := string(p.buf) var err error switch len(p.wrappedErrs) { case 0: err = errors.New(s) case 1: w := &wrapError{msg: s} w.err, _ = a[p.wrappedErrs[0]].(error) err = w default: if p.reordered { slices.Sort(p.wrappedErrs) } var errs []error for i, argNum := range p.wrappedErrs { if i > 0 && p.wrappedErrs[i-1] == argNum { continue } if e, ok := a[argNum].(error); ok { errs = append(errs, e) } } err = &wrapErrors{s, errs} } p.free() return err}
写在最后
本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!下一篇我们看看sync。
夜雨聆风
