乐于分享
好东西不私藏

一起读Go源码(第12期) fmt包

一起读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. 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. 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. 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.Iserrors.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。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 一起读Go源码(第12期) fmt包

评论 抢沙发

6 + 7 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮