golang 使用Stringer优化调试输出

75 阅读4分钟

解释

在golang中,Stringer是一个接口,通常由具有自定义字符串表示形式的类型实现。Stringer接口定义了一个名为 String 的方法,该方法返回该类型的字符串表示形式。这使得在使用这些类型时,可以方便地通过 fmt 包的打印函数输出自定义格式的字符串。

接口定义

//代码位于 GOROOT/src/fmt/print.go L:58

// Stringer is implemented by any value that has a String method,
// which defines the “native” format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
	String() string
}

可以看到这个定义很简单,只需要实现一个string方法即可,写个demo试一下

package main

import "fmt"

type user struct {
	Id     int    `json:"id"`
	Name   string `json:"name"`
	Status state  `json:"status"`
}

type state int

func (s state) String() string {
	switch s {
	case 1:
		return "启用"
	case 2:
		return "停用"
	default:
		return "未知"
	}
}

func main() {
	u := user{
		Id:     1,
		Name:   "xiaochuan",
		Status: 1,
	}

	fmt.Println(u)
}

上面代码我们定义了一个state类型用来标识状态,state实现了Stringer接口将对应的状态int值转换为汉字输出,执行效果如下:

image.png

可以看到我们的设置的状态已经被转换为了最终的中文状态表示,具体底层是怎么调度的我们看一下源码

底层源码调度

Println函数

//代码位于 GOROOT/src/fmt/print.go L:313
func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

Fprintln函数

//代码位于 GOROOT/src/fmt/print.go L:302
func Fprintln(w io.Writer, a ...any) (n int, err error) {
        p := newPrinter()
        p.doPrintln(a)
        n, err = w.Write(p.buf)
        p.free()
        return
}

doPrintln函数

循环执行我们的传参,因为上层是可变变量传参,所以到这是个数组循环处理

//代码位于 GOROOT/src/fmt/print.go L:1218
func (p *pp) doPrintln(a []any) {
        for argNum, arg := range a {
                if argNum > 0 {
                        p.buf.writeByte(' ')
                }
                p.printArg(arg, 'v')
        }
        p.buf.writeByte('\n')
}

printArg

最终逻辑调度到default分支,handleMethods中会进行断言判断

//代码位于 GOROOT/src/fmt/print.go L:681
func (p *pp) printArg(arg any, verb rune) {
        p.arg = arg
        p.value = reflect.Value{}

        if arg == nil {
                switch verb {
                case 'T', 'v':
                        p.fmt.padString(nilAngleString)
                default:
                        p.badVerb(verb)
                }
                return
        }

        // Special processing considerations.
        // %T (the value's type) and %p (its address) are special; we always do them first.
        switch verb {
        case 'T':
                p.fmt.fmtS(reflect.TypeOf(arg).String())
                return
        case 'p':
                p.fmtPointer(reflect.ValueOf(arg), 'p')
                return
        }

        // Some types can be done without reflection.
        switch f := arg.(type) {
        case bool:
                p.fmtBool(f, verb)
        case float32:
                p.fmtFloat(float64(f), 32, verb)
        case float64:
                p.fmtFloat(f, 64, verb)
        case complex64:
                p.fmtComplex(complex128(f), 64, verb)
        case complex128:
                p.fmtComplex(f, 128, verb)
        case int:
                p.fmtInteger(uint64(f), signed, verb)
        case int8:
                p.fmtInteger(uint64(f), signed, verb)
        case int16:
                p.fmtInteger(uint64(f), signed, verb)
        case int32:
                p.fmtInteger(uint64(f), signed, verb)
        case int64:
                p.fmtInteger(uint64(f), signed, verb)
        case uint:
                p.fmtInteger(uint64(f), unsigned, verb)
        case uint8:
                p.fmtInteger(uint64(f), unsigned, verb)
        case uint16:
                p.fmtInteger(uint64(f), unsigned, verb)
        case uint32:
                p.fmtInteger(uint64(f), unsigned, verb)
        case uint64:
                p.fmtInteger(f, unsigned, verb)
        case uintptr:
                p.fmtInteger(uint64(f), unsigned, verb)
        case string:
                p.fmtString(f, verb)
        case []byte:
                p.fmtBytes(f, verb, "[]byte")
        case reflect.Value:
                // Handle extractable values with special methods
                // since printValue does not handle them at depth 0.
                if f.IsValid() && f.CanInterface() {
                        p.arg = f.Interface()
                        if p.handleMethods(verb) {
                                return
                        }
                }
                p.printValue(f, verb, 0)
        default: 
                // If the type is not simple, it might have methods.
                if !p.handleMethods(verb) {
                        // Need to use reflection, since the type had no
                        // interface methods that could be used for formatting.
                        p.printValue(reflect.ValueOf(f), verb, 0)
                }
        }
}

handleMethods

可以看到在函数的最后进行了类型的断言,判断是否实现了error与Stringer接口,如果实现了Stringer接口就会调度string函数,这个就是我们上面示例代码自己写的用户层代码

//代码位于 GOROOT/src/fmt/print.go L:621
func (p *pp) handleMethods(verb rune) (handled bool) {
        if p.erroring {
                return
        }
        if verb == 'w' {
                // It is invalid to use %w other than with Errorf or with a non-error arg.
                _, ok := p.arg.(error)
                if !ok || !p.wrapErrs {
                        p.badVerb(verb)
                        return true
                }
                // If the arg is a Formatter, pass 'v' as the verb to it.
                verb = 'v'
        }

        // Is it a Formatter?
        if formatter, ok := p.arg.(Formatter); ok {
                handled = true
                defer p.catchPanic(p.arg, verb, "Format")
                formatter.Format(p, verb)
                return
        }

        // If we're doing Go syntax and the argument knows how to supply it, take care of it now.
        if p.fmt.sharpV {
                if stringer, ok := p.arg.(GoStringer); ok {
                        handled = true
                        defer p.catchPanic(p.arg, verb, "GoString")
                        // Print the result of GoString unadorned.
                        p.fmt.fmtS(stringer.GoString())
                        return
                }
        } else {
                // If a string is acceptable according to the format, see if
                // the value satisfies one of the string-valued interfaces.
                // Println etc. set verb to %v, which is "stringable".
                switch verb {
                case 'v', 's', 'x', 'X', 'q':
                        // Is it an error or Stringer?
                        // The duplication in the bodies is necessary:
                        // setting handled and deferring catchPanic
                        // must happen before calling the method.
                        switch v := p.arg.(type) { //断言判断,Stringer类型直接调度string函数
                        case error:
                                handled = true
                                defer p.catchPanic(p.arg, verb, "Error")
                                p.fmtString(v.Error(), verb)
                                return

                        case Stringer:
                                handled = true
                                defer p.catchPanic(p.arg, verb, "String")
                                p.fmtString(v.String(), verb)
                                return
                        }
                }
        }
        return false
}

总结

在日常开发中,我们会用到数值类型来表示一些标识,日常开发的久了突然看到某个数值不一定能直接想起这个值表示的含义是什么,我们可以通过在代码中实现Stringer接口,这样打印的数值会替换成具体的含义方便debug时进行分析判断出了什么问题,不用再进行文档的查找含义查找。