小白学标准库之 flag

Posted 乱舞春秋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小白学标准库之 flag相关的知识,希望对你有一定的参考价值。


Go 提供了解析命令行参数的 flag 包,本文旨在介绍 flag 的使用及内部实现等。

1. flag 包使用及实现

type PropertyOfPod struct {
	Namespace *string
	PodName   *string
	Phase     *string
}

var pod = PropertyOfPod{}

func init() {
    // String defines a string flag with specified name, default value, and usage string.
    // The return value is the address of a string variable that stores the value of the flag.

	pod.Namespace = flag.String("namespace", "default", "resource field for pod")
	pod.PodName = flag.String("name", "", "pod name")

	pod.Phase = new(string)

    // StringVar defines a string flag with specified name, default value, and usage string.
    // The argument p points to a string variable in which to store the value of the flag.

	flag.StringVar(pod.Phase, "phase", "running", "pod phase")
}

func main() {
	flag.Parse()

	fmt.Printf("pod property:\\nnamespace: %v, pod name: %v, phase: %v\\n", *pod.Namespace, *pod.PodName, *pod.Phase)
}

调用 flag 包的 String 函数定义 flag。示例中,通过 String 和 StringVar 两种方式定义 flag。

具体定义 flag 的 String 函数做了什么呢?接着往下看 String 函数:

func String(name string, value string, usage string) *string {
	return CommandLine.String(name, value, usage)
}

String 通过实例化类 CommandLine 调用 String 方法。其中 CommandLine 的类结构体定义为:

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

type FlagSet struct {
	Usage func()

	name          string                                // 每个 FlagSet 有唯一的 name
	parsed        bool                                  // parsed 标志记录 FlagSet 是否解析命令行参数
	actual        map[string]*Flag                      // 最终记录的实际可用标志
	formal        map[string]*Flag                      // 标志信息,未“过滤”
	args          []string // arguments after flags     // 命令行传入参数 os.Args[1:] 写入到 args 中
	errorHandling ErrorHandling
	output        io.Writer // nil means stderr; use Output() accessor
}

FlagSet 的 String 方法又做了什么呢?接着往下看:

func (f *FlagSet) String(name string, value string, usage string) *string {
	p := new(string)
	f.StringVar(p, name, value, usage)
	return p
}

func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
	f.Var(newStringValue(value, p), name, usage)
}

在 String 方法内创建了临时指针变量 p, p 的值被传入到 StringVar 函数。这和示例直接调用 StringVar 是一样的,在 newStringValue 函数中,p 将指向 value 的地址:

// -- string Value
type stringValue string

func newStringValue(val string, p *string) *stringValue {
	*p = val
	return (*stringValue)(p)
}

最后调用 Var 方法实现 flag 的定义,Var 方法的第一个参数值得一说,它接受的是接口类型 Value 的值,为什么接受接口类型是因为这里需要多态实现不仅定义 String flag,也能定义 Int,Bool 等类型的 flag。

Var 方法将外部传入参数定义到 Flag 结构体中,并作为值赋给 formal:

func (f *FlagSet) Var(value Value, name string, usage string) {
	flag := &Flag{name, usage, value, value.String()}
	_, alreadythere := f.formal[name]
	if alreadythere {
		var msg string
		if f.name == "" {
			msg = fmt.Sprintf("flag redefined: %s", name)
		} else {
			msg = fmt.Sprintf("%s flag redefined: %s", f.name, name)
		}
		fmt.Fprintln(f.Output(), msg)
		panic(msg) // Happens only if flags are declared with identical names
	}
	if f.formal == nil {
		f.formal = make(map[string]*Flag)
	}
	f.formal[name] = flag
}

定义了 flag 之后,还需要 parse 对传入的 flag 进行解析,实例中调用 flag 的 Parse 函数实现解析:

func Parse() {
	// Ignore errors; CommandLine is set for ExitOnError.
	CommandLine.Parse(os.Args[1:])
}

func (f *FlagSet) Parse(arguments []string) error {
	f.parsed = true                 // parse 时将 parsed 标志记为 true
	f.args = arguments
	for {
		seen, err := f.parseOne()
		if seen {
			continue
		}
		if err == nil {
			break
		}
		...
	}
	return nil
}

其中,parseOne 方法解析 args 参数中的 flag,并将解析的 flag 赋给 map actual。

2. flag 方法

2.1 Visit

// Visit visits the command-line flags in lexicographical order, calling fn
// for each. It visits only those flags that have been set.

flag.Visit(func(f *flag.Flag) {
		key := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
		Flag_visit[key] = string(f.Value.String())
	})
	fmt.Printf("Flag_visit: %v\\n", Flag_visit)

2.2 VisitAll

// VisitAll visits the command-line flags in lexicographical order, calling
// fn for each. It visits all flags, even those not set.

flag.VisitAll(func(f *flag.Flag) {
		key := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
		Flag_visit[key] = string(f.Value.String())
	})
	fmt.Printf("Flag_visit: %v\\n", Flag_visit)
芝兰生于空谷,不以无人而不芳。

小白学标准库之 log


日常开发中,日志 log 几乎是必不可少。本文旨在介绍 log 的使用和内部实现等。

1. log 使用及实现

package main

import (
	"fmt"
	"log"
)

func init() {
	log.SetPrefix("Trace: ")
	log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}

func main() {
	log.Println("message")

	log.Fatalln("fatal message")

    log.Panicln("panic message")
}

使用 log 需要用到标准库 log 包。init 函数中的 SetPrefix 和 SetFlags 函数定义了 log 的输出格式。以 SetFlags 为例,查看函数原型:

func SetFlags(flag int) {
	std.SetFlags(flag)
}

函数中 std 为 Logger 结构体变量,为指定 Logger 结构体变量,标准库会实例化默认结构体变量:

var std = New(os.Stderr, "", LstdFlags)

// New 函数
func New(out io.Writer, prefix string, flag int) *Logger {
	return &Logger{out: out, prefix: prefix, flag: flag}
}

// Logger 结构体
type Logger struct {
	mu     sync.Mutex // ensures atomic writes; protects the following fields
	prefix string     // prefix on each line to identify the logger (but see Lmsgprefix)
	flag   int        // properties
	out    io.Writer  // destination for output
	buf    []byte     // for accumulating text to write
}

值得一提的是,New 函数接受一个 io.Writer 的接口类型变量,该变量定义了 log 日志的输出路径,默认输出到标准设备 os.Stderr。Stderr 的定义为:

package os

var (
	Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
	Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
	Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

func NewFile(fd uintptr, name string) *File

在包 os 中可以看到三种 File 引用类型的标准输出设备定义 Stdin, Stdout, Stderr。

拉回来我们继续看 std.SetFlags(flag) 这个方法,这个方法很有意思,它接受一个整数值,可是外面传递给 SetFlags 是二进制或类型的值:log.Ldate | log.Lmicroseconds | log.Llongfile。这中间发生了什么呢?

我们在这里找到了答案:

const (
	Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23
	Ltime                         // the time in the local time zone: 01:23:23
	Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
	Llongfile                     // full file name and line number: /a/b/c/d.go:23
	Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
	LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
	Lmsgprefix                    // move the "prefix" from the beginning of the line to before the message
	LstdFlags     = Ldate | Ltime // initial values for the standard logger
)

这里列举了枚举类型常量,且通过特殊常量 iota 实现了不同常量处于不同的二进制位,从而能区分出不同标志位,实现不同的 log 输出信息打印。关于不同标志位的判断是在 formatHeader 方法实现的,这里就不展开介绍了。

l.formatHeader(&l.buf, now, file, line)

在 log 打印这里,调用 Println, Fatalln, Panicln 函数实现不同类型日志打印。Println 输出写到标准输出设备中,Fatalln 输出在调用 Println 方法后调用 os.Exit(1) 退出程序执行,Panicln 在调用 Println 方法后继续调用 panic()。

其中的核心是 Output 方法如下:

func (l *Logger) Output(calldepth int, s string) error {
	now := time.Now() // get this early.
	var file string
	var line int
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.flag&(Lshortfile|Llongfile) != 0 {
		// Release lock while getting caller info - it\'s expensive.
		l.mu.Unlock()
		var ok bool
		_, file, line, ok = runtime.Caller(calldepth)
		if !ok {
			file = "???"
			line = 0
		}
		l.mu.Lock()
	}
	l.buf = l.buf[:0]
	l.formatHeader(&l.buf, now, file, line)
	l.buf = append(l.buf, s...)
	if len(s) == 0 || s[len(s)-1] != \'\\n\' {
		l.buf = append(l.buf, \'\\n\')
	}
	_, err := l.out.Write(l.buf)
	return err
}

简要介绍该方法:

  • 使用同步锁防止 goroutine 之间的写入竞争状态。
  • formatHeader 将输入的格式按顺序排列。
  • 结构体 l 的 out io.Write 接口变量调用 Write 方法实现日志信息写入。

2. 自定义 log

上节使用的是默认 Logger 类型,也可以根据不同 Logger 类型定义不同日志记录器:

var (
	Trace   *log.Logger
	Info    *log.Logger
	Warning *log.Logger
	Error   *log.Logger
)

这里定义了四种 Logger 类型的日志记录器,分别记录 Trace,Info,Warning 和 Error 类型的日志。

完整代码如下:

var (
	Trace   *log.Logger
	Info    *log.Logger
	Warning *log.Logger
	Error   *log.Logger
)

func init() {
	file, err := os.OpenFile("C:/Data/chunqiu/Software/errors.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("Failed to open error log file: ", err)
	}

	Trace = log.New(ioutil.Discard, "Trace: ", log.Ldate|log.Lmicroseconds|log.Llongfile)
	Info = log.New(os.Stdout, "Info: ", log.Ldate|log.Lmicroseconds|log.Llongfile)
	Warning = log.New(os.Stdout, "WARNING: ", log.Ldate|log.Lmicroseconds|log.Llongfile)
	Error = log.New(io.MultiWriter(file, os.Stderr), "ERROR: ", log.Ldate|log.Lmicroseconds|log.Llongfile)
}

func main() {
	Trace.Println("I have a dream")
	Info.Println("I have a dream too")
	Warning.Println("No, you haven\'t")
	Error.Println("Yes, Warning is right")
}

这里需要注意的是:

  1. 调用 os 的 OpenFile 函数定义了输出文件信息,输出到指定目录的 errors.txt 文件中,且文件权限为 666。在 Error 日志记录器中,将该文件作为 Write 接口变量闯入 New 函数中。
  2. Trace 日志记录器调用 ioutil 的 Discard 变量,该变量是一个实现了 Write 接口的 Discard 结构体变量,Dirscard 什么都没定义。实现的效果是禁用这种类型的日志输出。
  3. Info 和 Warning 的日志将写入到标准输出设备 Stdout 中。
  4. MultiWriter 是一个接受可变参数的函数,该函数将多个可变参数组合在切片中,赋值给 multiWriter 结构体,该结构体实现了 Writer 接口类型详细定义如下:
    func MultiWriter(writers ...Writer) Writer {
     allWriters := make([]Writer, 0, len(writers))
     for _, w := range writers {
     	if mw, ok := w.(*multiWriter); ok {
     		allWriters = append(allWriters, mw.writers...)
     	} else {
     		allWriters = append(allWriters, w)
     	}
     }
     return &multiWriter{allWriters}
     }
     不展开细讲,最终的表现形式是日志可以写入到外部传入的多个输出设备中,这里输出到 file 和 os.Stderr 中。
    

最后检查输出打印和 errors.txt 信息如下:

C:\\Users\\chunqiu\\go>go run "c:\\Users\\chunqiu\\go\\src\\goinaction\\lib\\log\\customized_log.go"
Info: 2021/09/28 00:59:17.648348 c:/Users/chunqiu/go/src/goinaction/lib/log/customized_log.go:31: I have a dream too
WARNING: 2021/09/28 00:59:17.648887 c:/Users/chunqiu/go/src/goinaction/lib/log/customized_log.go:32: No, you haven\'t    
ERROR: 2021/09/28 00:59:17.648887 c:/Users/chunqiu/go/src/goinaction/lib/log/customized_log.go:33: Yes, Warning is right

// errors.txt
ERROR: 2021/09/27 23:36:08.177438 c:/Users/chunqiu/go/src/goinaction/lib/log/customized_log.go:33: Yes, Warning is right
芝兰生于空谷,不以无人而不芳。

以上是关于小白学标准库之 flag的主要内容,如果未能解决你的问题,请参考以下文章

小白学标准库之 mux

Go语言标准库之flag

go语言标准库之flag

Go语言标准库之命令行参数的解析:flag 库详解

Go语言标准库之命令行参数的解析:flag 库详解

python标准库 —— os模块