Go 源码阅读之 flag 包

Stella981
• 阅读 703

Go 源码阅读系列是我的源码阅读笔记。因为本人的电脑上 Go 的版本是1.13.4,所以就选择了该版本作为学习的版本。为此我在 Github 上 Fork 了 Go 的源码,并创建了 study1.13.4 分支,来记录对于源码的个人理解或者说中文注释也行。每当阅读完一个包后都会进行一下小结,就像这篇是对flag包的总结整理。当然在整理的过程中发现 Go夜读系列视频,也让我受益颇多。

  • 简介
  • 文件结构
  • 运行测试
  • 总结
    • 接口转换能实现类似 C++ 中模板的功能
    • 函数 vs 方法
    • new vs make
    • 指针赋值给接口变量
    • flag文件夹中有flag_test
    • 作用域
  • 后续深入TODO
  • 参考文献

简介

flag 包是 Go 里用于解析命令行参数的包。为什么选择它作为第一个阅读的包,因为它的代码量少。其核心代码只有一个 1000 不到的 flag.go 文件。

文件结构

flag 包的文件结构很简单,就一层。一个文件夹里放了 5 个文件,其文件及其作用如下:

  • flag.go

    flag 的核心包,实现了命令行参数解析的所有功能

  • export_test.go

    测试的实用工具,定义了所有测试需要的基础变量和函数

  • flag_test.go

    flag 的测试文件,包含了 17 个测试单元

  • example_test.go

    flag 的样例文件,介绍了 flag 包的三种常用的用法样例

  • example_value_test.go

    flag 的样例文件,介绍了一个更复杂的样例

运行测试

我先介绍一下 Go 的运行环境。

# 通过 brew install go 安装,源码位置为 $GOROOT/src
GOROOT=/usr/local/opt/go/libexec
# 阅读的源码通过 go get -v -d github.com/haojunyu/go 下载,源码位置为 $GOPATH/src/github.com
GOPATH=$HOME/go

单独测试 flag 包踩过的坑:

  1. 无法针对单个文件进行测试,需要针对包。

这里重点说一下 export_test.go 文件,它是flag包的一部分package flag,但是它确实专门为测试而存在的,说白了也就一个ResetForTesting方法,用来清除所有命令参数状态并且直接设置Usage函数。该方法会在测试用例中被频繁使用。所以单独运行以下命令会报错"flag_test.go:30:2: undefined: ResetForTesting"

# 测试当前目录(报错)
go test -v .
# 测试包
go test -v flag
  1. go test -v flag 测试的源码是 $GOROOT/src 下的(以我当前的测试环境)

指定 flag 包后,实际运行的源码是 $GOROOT 下的,这个应该和我的安装方式有关系。

总结

接口转换能实现类似 C++ 中模板的功能

flag 包中定义了一个结构体类型叫 Flag,它用来存放一个命令参数,其定义如下。

// A Flag represents the state of a flag.
// 结构体Flag表示一个参数的所有信息,包括名称,帮助信息,实际值和默认值
type Flag struct {
    Name     string // name as it appears on command line名称
    Usage    string // help message帮助信息
    Value    Value  // value as set实现了取值/赋值方法的接口
    DefValue string // default value (as text); for usage message默认值
}

其中命令参数的值是一个 Value 接口类型,其定义如下:

// Set is called once, in command line order, for each flag present.
// The flag package may call the String method with a zero-valued receiver,
// such as a nil pointer.
// 接口Value是个接口,在结构体Flag中用来存储每个参数的动态值(参数类型格式各样)
type Value interface {
    String() string   // 取值方法
    Set(string) error // 赋值方法
}

为什么这么做?因为这样做能够实现类似模板的功能。任何一个类型 T 只要实现了 Value 接口里的 StringSet 方法,那么该类型 T 的变量 v 就可以转换成 Value 接口类型,并使用 String 来取值,使用 Set 来赋值。这样就能完美的解决不同类型使用相同的代码操作目的,和 C++ 中的模板有相同的功效。

函数 vs 方法

函数和方法都是一组一起执行一个任务的语句,二者的区别在于调用者不同,函数的调用者是包 package,而方法的调用者是接受者 receiver。在 flag 的源码中,有太多的函数里面只有一行,就是用包里的变量 CommandLine 调用同名方法。

// Parsed reports whether f.Parse has been called.
// Parsed方法: 命令行参数是否已经解析
func (f *FlagSet) Parsed() bool {
    return f.parsed
}

// Parsed reports whether the command-line flags have been parsed.
func Parsed() bool {
    return CommandLine.Parsed()
}

new vs make

newmake 是 Go 语言中两种内存分配原语。二者所做的事情和针对的类型都不一样。 new 和其他编程语言中的关键字功能类似,都是向系统申请一段内存空间来存储对应类型的数据,但又有些区别,区别在于它会将该片空间置零。也就是说 new(T) 会根据类型 T 在堆上 申请一片置零的内存空间,并返回指针 *Tmake 只针对切片,映射和信道三种数据类型 T 的构建,并返回类型为 T 的一个已经初始化(而非零)的值。原因是这三种数据类型都是引用数据类型,在使用前必须初始化。就像切片是一个具有三项内容的描述符,包含一个指向数组的指针,长度和容量。通过 make 创建对应类型的变量过程是先分配一段空间,接着根据对应的描述符来创建对应的类型变量。关于 make 的细节可以看 draveness 写的 Go语言设计与实现。

// Bool defines a bool flag with specified name, default value, and usage string.
// The return value is the address of a bool variable that stores the value of the flag.
func (f *FlagSet) Bool(name string, value bool, usage string) *bool {
    p := new(bool)
    f.BoolVar(p, name, value, usage)
    return p
}


// sortFlags returns the flags as a slice in lexicographical sorted order.
// sortFlags函数:按字典顺序排序命令参数,并返回Flag的切片
func sortFlags(flags map[string]*Flag) []*Flag {
    result := make([]*Flag, len(flags))
    i := 0
    for _, f := range flags {
        result[i] = f
        i++
    }
    sort.Slice(result, func(i, j int) bool {
        return result[i].Name < result[j].Name
    })
    return result
}

指针赋值给接口变量

Go 中的接口有两层含义,第一层是一组方法(不是函数)的签名,它需要接受者(具体类型 T 或具体类型指针 *T )来实现细节;另一层是一个类型,而该类型能接受所有现实该接受的接受者。深入理解接口的概念可以细读 Go语言设计与实现之接口。在 flag 包中的 StringVar 方法中newStringValue(value, p)返回的是 *stringValue 类型,而该类型(接受者)实现了 Value 接口( StringSet 方法),此时该类型就可以赋值给 Value 接口变量。

// 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.
// StringVar方法:将命令行参数的默认值value赋值给变量*p,并生成结构Flag并置于接受者中f.formal
func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
    f.Var(newStringValue(value, p), name, usage) // newStringValue返回值是*stringValue类型,之所以能赋值给Value接口是因为newStringValue实现Value接口时定义的接受者为*stringValue
}

flag文件夹中有flag_test

flag 文件夹下有 flag_test 包,是因为该文件夹下包含了核心代码 flag.go 和测试代码 *_test.go 。这两部分代码并没有通过文件夹来区分。所以该 flag_test 包存在的意义是将测试代码与核心代码区分出来。而该包被引用时只会使用到核心代码。

// example_test.go
package flag_test

作用域

关于作用域 Golang变量作用域 和 GO语言圣经中关于作用域 都有了详细的介绍,前者更通俗易懂些,后者更专业些。在 flag 包的 TestUsage 测试样例中,因为 func(){called=true} 是在函数 TestUsage 中定义函数,并且直接作为形参传递给 ResetForTesting 函数,所以该函数是和局部变量 called 是同级的,当然在该函数中给该变量赋值也是合理的。

//  called变量的作用域
func TestUsage(t *testing.T) {
    called := false
    // 变量called的作用域
    ResetForTesting(func() { called = true })
    if CommandLine.Parse([]string{"-x"}) == nil {
        t.Error("parse did not fail for unknown flag")
    } else {
        t.Error("hahahh")
    }
    if !called {
        t.Error("did not call Usage for unknown flag")
    }
}

后续深入TODO

  • [ ] go test 测试原理
  • [ ] 接口转换原理
  • [ ] 反射

参考文献

  1. Go 夜读之 flag 包视频
  2. 实效 Go 编程之内存分配
  3. Go 语言设计与实现之 make 和 new
  4. 菜鸟教程之 Go 语言变量作用域
  5. Go 语言圣经中关于作用域
  6. Go 语言中值 receiver 和指针 receiver 的对比
  7. Go CodeReviewComments
  8. Golang 变量作用域
  9. Go 语言圣经中关于作用域
  10. Go 语言设计与实现之接口

如果该文章对您产生了帮助,或者您对技术文章感兴趣,可以关注微信公众号: 技术茶话会, 能够第一时间收到相关的技术文章,谢谢! Go 源码阅读之 flag 包

本篇文章由一文多发平台ArtiPub自动发布

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
科工人 科工人
3年前
go的三个运行基本命令的区别,go run ,go build 和 go install
最近在自学go,遇到点基础的问题,通过自己实际操作之后得出结论在实际操作之前,我们需要知道go有三种源码文件:   1,命令源码文件;声明自己属于main包,并且包含main函数的文件,每个项目只能有一个这样的文件,即程序的入口文件   2,库源码文件;不能直接被执行的源码文件   3,测试源码文件本次操作不涉及测试源码文件。gorun
Easter79 Easter79
3年前
sync.Once
今天阅读go部分源码的时候发现了一个包sync.Once那么这个包来干什么的呢?通过百度和查看源码得知sync.Once可以控制函数只能被调用一次。不能多次重复调用。varconfOncesync.OnceconfOnce.Do(func(){log.Println("test")})
捉虫大师 捉虫大师
3年前
Sentinel-Go 源码系列(一)|开篇
大家好呀,打算写一个Go语言组件源码分析系列,一是为了能学习下Go语言,看下别人是怎么写Go的,二是也掌握一个组件。本次选择了SentinelGo,一是对Java版本的Sentinel算是有一些了解,也在生产上落地过,二是感觉他的代码应该不会太复杂(仅仅是感觉),三是在云原生越来越热的趋势下,用Go实现的限流降级容错应该是比较通用的
Stella981 Stella981
3年前
MacOS下安装小米SQL优化工具soar
1下载源码包赋予权限  wgethttps://github.com/XiaoMi/soar/releases/download/0.11.0/soar.darwinamd64Osoar  chmodaxsoar2安装Go语言brewinstallygo发现版本不对,直接去https:/
Wesley13 Wesley13
3年前
Java GC详解
最近在抽时间阅读JDK的源码,主要是GC还有Safepoint相关的源码,发现很多我在之前拜读网上各种JVM原理大作时候由于没有看源码导致我对于底层原理的误解。果然,一百个人读水浒传,就有一百种水浒传。还是需要更加深入的了解下源码,才能更好地理解JVM,进行调优。这个系列,将在讲述JavaGC各种原理的基础上,结合对应的源码分析,
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
wc 统计已过时,cloc 每一行代码都有效
上下文我们通常用代码量来评判一个程序员技术的高低或一个项目的大小,阅读新项目源码时,也需要了解其代码量,心里有个预期。那如何统计代码量呢?传统做法通过findwc命令完成shell$find.name".go"|xargswcl60./etcd/tutorial/distributedlock.go...3491total如上,统计命
京东云开发者 京东云开发者
5个月前
spring源码阅读之bean加载过程(一)
如果想要阅读源码,首先要选择版本,然后将源代码下载到本地,导入idea中,话不多说,直接看步骤吧这里我选择5版本,下载源码默认是main分支,看想学习的分支,比如我切换到5版本,截图如下:2.安装gradle3.转换源码进idea下载完成后可以看到有个文档