Golang Source Code Analysis: How is fmt Standard Library-Print * Output?

  golang, php

Golang Source Code Analysis: How is fmt Standard Library-Print * Output?

Original address:Golang Source Code Analysis: fmt Standard Library

Preface

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World!")
}

I’ve seen a lot of standard introductions. How did the internal standard library export this English? Let’s watch the source code today

Prototype

func Print(a ...interface{}) (n int, err error) {
    return Fprint(os.Stdout, a...)
}

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

func Printf(format string, a ...interface{}) (n int, err error) {
    return Fprintf(os.Stdout, format, a...)
}
  • Print: Prints the format using the default format specifier and writes to standard output. When neither is a string, add a space between the operands
  • Println: Same as above, except that spaces are always added between operands and line breaks are added.
  • Printf: Formats according to format specifier and writes to standard output

The above three categories are the most common methods for formatting I/O, and we will describe the disassembly based on this.

Execution process

Case 1: Print

Here we usePrintMethods To make an analysis for further understanding.

func Print(a ...interface{}) (n int, err error) {
    return Fprint(os.Stdout, a...)
}

PrintPrints the format using the default format specifier and writes to standard output. In addition, a space will be inserted when both are non-empty strings.

Prototype

func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrint(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

The function has two formal parameters:

  • W: Output stream. As long as io.Writer is implemented, it can be (abstractly) written to the stream.
  • A: multiple values of any type

Analyze backbone processes

1. p := newPrinter (): apply for a temporary object pool (sync.Pool)

var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.fmt.init(&p.buf)
    return p
}
  • PpFree.Get (): based on sync.Pool, a temporary object pool of *pp is implemented, and a new pp object will be returned for subsequent processing every time it is acquired.
  • *pp.panicking: used to solve the problems of infinite recursion panic and recover. According to this parameter, the catchPanic will be switched off in time.
  • *pp.erroring: verb identifier used to indicate that the error being processed is invalid. Its main function is to prevent the handleMethods method from being called.
  • *pp.fmt.init(&p.buf): initialize fmt configuration, buf will be set and fmtFlags flag bit will be cleared.

2. p.doPrint(a): perform the agreed formatting action (add a space between parameters and a line break for the last parameter)

func (p *pp) doPrint(a []interface{}) {
    prevString := false
    for argNum, arg := range a {
        true && false
        isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
        // Add a space between two non-string arguments.
        if argNum > 0 && !isString && !prevString {
            p.buf.WriteByte(' ')
        }
        p.printArg(arg, 'v')
        prevString = isString
    }
}

It can be seen that the bottom layer judges the input parameter,At the same timeDelimiters (spaces) are added when the following conditions are met:

  • The current reference is multiple parameters (e.g. Slice)
  • The current input parameter is not nil and is not a string (determined by reflection)
  • The current entry is not a prefix or the previous entry is not a string.

And inPrintMethod, you do not need to specify a formatter. In fact, it is directly specified asv. This is the value in the default format

p.printArg(arg, 'v')
  1. W.Write(p.buf): write standard output (io.Writer)
  2. *pp.free (): frees cached content. After the temporary object is used, buf, arg, and value will be emptied and stored in ppFree again. So that reuse can be taken out later (using the temporary object feature of sync.Pool)

Case 2: Printf

Identifier

Verbs
%v    the value in a default format
    when printing structs, the plus flag (%+v) adds field names
%#v    a Go-syntax representation of the value
%T    a Go-syntax representation of the type of the value
%%    a literal percent sign; consumes no value
%t    the word true or false
Flags
+    always print a sign for numeric values;
    guarantee ASCII-only output for %q (%+q)
-    pad with spaces on the right rather than the left (left-justify the field)
#    alternate format: add leading 0 for octal (%#o), 0x for hex (%#x);
    0X for hex (%#X); suppress 0x for %p (%#p);
    for %q, print a raw (backquoted) string if strconv.CanBackquote
    returns true;
    always print a decimal point for %e, %E, %f, %F, %g and %G;
    do not remove trailing zeros for %g and %G;
    write e.g. U+0078 'x' if the character is printable for %U (%#U).
' '    (space) leave a space for elided sign in numbers (% d);
    put spaces between bytes printing strings or slices in hex (% x, % X)
0    pad with leading zeros rather than spaces;
    for numbers, this moves the padding after the sign

For detailed suggestions, seeGodoc

Prototype

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintf(format, a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

Compared with Print, the biggest difference is the doPrintf method. Here let’s take a closer look at its code, as follows:

func (p *pp) doPrintf(format string, a []interface{}) {
    end := len(format)
    argNum := 0         // we process one argument per non-trivial format
    afterIndex := false // previous item in format was an index like [3].
    p.reordered = false
formatLoop:
    for i := 0; i < end; {
        p.goodArgNum = true
        lasti := i
        for i < end && format[i] != '%' {
            i++
        }
        if i > lasti {
            p.buf.WriteString(format[lasti:i])
        }
        if i >= end {
            // done processing format string
            break
        }

        // Process one verb
        i++

        // Do we have flags?
        p.fmt.clearflags()
    simpleFormat:
        for ; i < end; i++ {
            c := format[i]
            switch c {
            case '#':   //'#'、'0'、'+'、'-'、' '
                ...
            default:
                if 'a' <= c && c <= 'z' && argNum < len(a) {
                    ...
                    p.printArg(a[argNum], rune(c))
                    argNum++
                    i++
                    continue formatLoop
                }
                
                break simpleFormat
            }
        }

        // Do we have an explicit argument index?
        argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))

        // Do we have width?
        if i < end && format[i] == '*' {
            ...
        }

        // Do we have precision?
        if i+1 < end && format[i] == '.' {
            ...
        }

        if !afterIndex {
            argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
        }

        if i >= end {
            p.buf.WriteString(noVerbString)
            break
        }

        ...

        switch {
        case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
            p.buf.WriteByte('%')
        case !p.goodArgNum:
            p.badArgNum(verb)
        case argNum >= len(a): // No argument left over to print for the current verb.
            p.missingArg(verb)
        case verb == 'v':
            ...
            fallthrough
        default:
            p.printArg(a[argNum], verb)
            argNum++
        }
    }

    if !p.reordered && argNum < len(a) {
        ...
    }
}

Analyze backbone processes

  1. Character content before writing%
  2. If all flag bits are processed (reaching the end of the character), the processing logic will jump out.
  3. (Move Back) Skip% to Start Processing Other verb Flag Bits
  4. Empty (Reinitialize) fmt Configuration
  5. Handle some basic verbformat. Such as’ #’,’ 0′,’+’,’-‘,’andA simple verbs identifier (excluding precision, width, and parameter index) . It should be noted that if the current character is a simple verb identifier. Processing is directly carried out. After completion, it will move directly back to the next character.. The remaining flag bits change fmt configuration items to facilitate subsequent processing.
  6. Processing parameter index
  7. Processing parameter width
  8. Precision of processing parameters
  9. After% returns if verbs identifier does not existnoVerbString. The value is%! (NOVERB)
  10. Handle special verbs identifiers (e.g.,’%’,’ %#v’,’ %+v’), error conditions (e.g., parameter index specifying error, number of parameter sets does not match number of verbs identifiers), or format parameter sets
  11. The routine process is completed

Under special circumstances, if more parameter sets are provided than verb identifiers. Fmt will examine greedily and output the extra parameter set in a specific format, as follows:

fmt.Printf("%d", 1, 2, 3)
// 1%!(EXTRA int=2, int=3)
  • Convention prefix extra flag:%! (EXTRA
  • The type of the current parameter
  • Convention formatter: =
  • The value of the current parameter (formatted with %v by default)
  • Convention formatter:)

It is worth noting that when the parameter index is specified or the actual processed parameter is smaller than the parameter set of the input parameter, greedy matching will not be performed to show it

Case 3: Println

Prototype

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

In this method, the biggest difference is doPrintln, let’s take a look, as follows:

func (p *pp) doPrintln(a []interface{}) {
    for argNum, arg := range a {
        if argNum > 0 {
            p.buf.WriteByte(' ')
        }
        p.printArg(arg, 'v')
    }
    p.buf.WriteByte('\n')
}

Analyze backbone processes

  • Loop in the parameter set of the parameter, separated by spaces
  • Formats the current parameter, defaults to%vFormat parameters
  • Add at end\nCharacter

How to format parameters

In the analysis of the execution flow in the above example, we can see that the step of formatting parameters is inp.printArg(arg, verb)Yes, let’s take a look at what it has done.

func (p *pp) printArg(arg interface{}, 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
    }

    switch verb {
    case 'T':
        p.fmt.fmt_s(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 reflect.Value:
        if f.IsValid() && f.CanInterface() {
            p.arg = f.Interface()
            if p.handleMethods(verb) {
                return
            }
        }
        p.printValue(f, verb, 0)
    default:
        if !p.handleMethods(verb) {
            p.printValue(reflect.ValueOf(f), verb, 0)
        }
    }
}

As can be seen in the section code, fmt itself has handled different types differently. In this way, confirmation by reflection is avoided. The performance is relatively improved

There are two special methods, namelyhandleMethodsAndbadVerbNext, let’s take a look at their roles respectively.

1、badVerb

It is mainly used to format and handle erroneous behaviors. Let’s look at it together. The code is as follows:

func (p *pp) badVerb(verb rune) {
    p.erroring = true
    p.buf.WriteString(percentBangString)
    p.buf.WriteRune(verb)
    p.buf.WriteByte('(')
    switch {
    case p.arg != nil:
        p.buf.WriteString(reflect.TypeOf(p.arg).String())
        p.buf.WriteByte('=')
        p.printArg(p.arg, 'v')
    ...
    default:
        p.buf.WriteString(nilAngleString)
    }
    p.buf.WriteByte(')')
    p.erroring = false
}

When dealing with error formatting, we can compare the following examples:

fmt.Printf("%s", []int64{1, 2, 3})
// [%!s(int64=1) %!s(int64=2) %!s(int64=3)]%

You can see in badVerb that the processing of error strings is mainly divided into the following parts:

  • Convention prefix error flag:%!
  • The current formatting operator
  • Convention formatter: (
  • The type of the current parameter
  • Convention formatter: =
  • The value of the current parameter (formatted with %v by default)
  • Convention formatter:)

2、handleMethods

func (p *pp) handleMethods(verb rune) (handled bool) {
    if p.erroring {
        return
    }
    // Is it a Formatter?
    if formatter, ok := p.arg.(Formatter); ok {
        handled = true
        defer p.catchPanic(p.arg, verb)
        formatter.Format(p, verb)
        return
    }

    // If we're doing Go syntax and the argument knows how to supply it, take care of it now.
    ...
    
    return false
}

This method is quite special and is usually called under the condition of custom structure and unknown conditions. The main processes are:

  • If the current parameter is the error verb identifier, it will be returned directly.
  • Judge whether Formatter has been implemented
  • Implemented, a custom Formatter is used to format the parameters
  • If it is not implemented, the default rule of Go syntax is used to format the parameters to the greatest extent.

Expand

In the fmt standard library, you can customize the method by customizing the structure, roughly as follows

fmt.State

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 is used to obtain the state value of the flag bit and involves the following:

  • Write: Writes the formatted characters into the buffer and waits for the next processing step.
  • Width: Returns width information and whether it is set
  • Precision: Returns precision information and whether it is set
  • Flag: Returns whether special identifiers (‘#’,’ 0′,’+’,’-‘,”) are set

fmt.Formatter

type Formatter interface {
    Format(f State, c rune)
}

Formatter is used to implementCustom formatting method. This can be achieved by implementing the Format method in a custom structure

In addition, state values such as the width and precision of the current identifier can be obtained through f. C is the verb identifier, and you can get what its action is.

fmt.Stringer

type Stringer interface {
    String() string
}

When the object is of type String, Array, Slice, etc., it will be calledString()The method formats the class string

fmt.GoStringer

type GoStringer interface {
    GoString() string
}

Called when formatting a specific verb identifier (%v)GoString()Method to format it

Summary

Through the analysis of fmt standard library, we can find that it has the following characteristics:

  • In terms of extensibility, you can customize formatting methods, etc.
  • In the aspect of completeness, greedy matching is carried out as much as possible, and parameter sets are output.
  • In terms of performance, each different parameter type implements different formatting operations.
  • In terms of performance, the shortest possible match is used to format the parameter set.

In general, the fmt standard library has many details worth considering, and I hope you can learn them in this article.


Original address:Golang Source Code Analysis: fmt Standard Library