教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Go源码阅读之flag包

Go源码阅读之flag包

发布时间:2022-03-04   编辑:jiaochengji.com
教程集为您提供Go源码阅读之flag包等资源,欢迎您收藏本站,我们将为您提供最新的Go源码阅读之flag包资源

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

<ul><li>简介</li><li>文件结构</li><li>运行测试</li><li>

总结

<ul><li>接口转换能实现类似 C 中模板的功能</li><li>函数 vs 方法</li><li><code>new</code> vs <code>make</code></li><li>指针赋值给接口变量</li><li>flag文件夹中有<code>flag_test</code>包</li><li>作用域</li></ul></li><li>后续深入TODO</li><li>参考文献</li></ul><h2>简介</h2>

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

<h2>文件结构</h2>

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

<ul><li>flag.go

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

</li><li>export_test.go

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

</li><li>flag_test.go

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

</li><li>example_test.go

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

</li><li>example_value_test.go

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

</li></ul><h2>运行测试</h2>

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

<pre><code class="lang-bash hljs"># 通过 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</code></code></pre>

单独测试 flag 包踩过的坑:

<ol><li>无法针对单个文件进行测试,需要针对包。</li></ol>

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

<pre><code class="lang-bash hljs"># 测试当前目录(报错) go test -v . # 测试包 go test -v flag</code></code></pre><ol><li><code>go test -v flag</code> 测试的源码是 <code>$GOROOT/src</code> 下的(以我当前的测试环境)</li></ol>

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

<h2>总结</h2><h3>接口转换能实现类似 C 中模板的功能</h3>

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

<pre><code class="lang-go hljs">// 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默认值 }</code></code></pre>

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

<pre><code class="lang-go hljs">// 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 // 赋值方法 }</code></code></pre>

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

<h3>函数 vs 方法</h3>

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

<pre><code class="lang-go hljs">// 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() }</code></code></pre><h3><code>new</code> vs <code>make</code></h3>

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

<pre><code class="lang-go hljs">// 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 }</code></code></pre><h3>指针赋值给接口变量</h3>

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

<pre><code class="lang-go hljs">// 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 }</code></code></pre><h3>flag文件夹中有<code>flag_test</code>包</h3>

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

<pre><code class="lang-go hljs">// example_test.go package flag_test</code></code></pre><h3>作用域</h3>

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

<pre><code class="lang-go hljs">// 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") } }</code></code></pre><h2>后续深入TODO</h2><ul><li>[ ] go test 测试原理</li><li>[ ] 接口转换原理</li><li>[ ] 反射</li></ul><h2>参考文献</h2><ol><li>Go 夜读之 flag 包视频</li><li>实效 Go 编程之内存分配</li><li>Go 语言设计与实现之 make 和 new</li><li>菜鸟教程之 Go 语言变量作用域</li><li>Go 语言圣经中关于作用域</li><li>Go 语言中值 receiver 和指针 receiver 的对比</li><li>Go CodeReviewComments</li><li>Golang 变量作用域</li><li>Go 语言圣经中关于作用域</li><li>Go 语言设计与实现之接口</li></ol>

如果该文章对您产生了帮助,或者您对技术文章感兴趣,可以关注微信公众号: 技术茶话会, 能够第一时间收到相关的技术文章,谢谢!
<span class="img-wrap"></span>

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


到此这篇关于“Go源码阅读之flag包”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Go源码阅读之flag包
Go 开发关键技术指南 | 为什么你要选择 Go?(内含超全知识大图)
golang key map 所有_golang系列——高级语法之map
Go语言爱好者周刊:第 78 期 — 这道关于 goroutine 的题
05.Go代码风格的唯一标准
想系统学习GO语言(Golang
Golang笔记:语法,并发思想,web开发,Go微服务相关
Go的隐秘世界:一个Goroutine要几个Thread
Go:Goroutine 的切换过程实际上涉及了什么
如何编写Go代码

[关闭]
~ ~