0%

Go 语言学习笔记(5):包结构、反射

这篇文章学习 Go 语言中的包结构,同时 Go 语言原生支持了反射,这里一并介绍。

包结构

工作空间

依照规范,工作空间(workspace)由 src、bin、pkg 三个目录组成。通常需要将空间路径添加到 GOPATH 环境列表中,以便相关工具能够正常工作(Go1.7 之前必须设置 GOPATH 这个变量,且不能够与 GOROOT 相同。1.8之后,GOPATH有默认值–$HOME/go)。

在工作空间里,包括子包在内的所有源码文件都保存在 src 目录中,而 bin、pkg 两个目录,主要影响 go install/get 命令中,它们会将编译结果(可执行文件或静态库)安装到这两个目录下,以实现增量编译。

编译器等相关工具按照 GOPATH 设置的路径搜索目标。在导入目标库时,排在列表前面的路径比当前工作空间优先级更高。另外,go get 默认将下载的第三方包保存到列表的第一个工作空间内。

环境变量 GOROOT 用于指示工具链和标准库的存放位置。在生成工具链时,相关路径就已经嵌入到可执行文件内,无需额外设置。除了通过设置 GOROOT 环境变量来覆盖内部路径外,还可以移动目录或重新编译工具链来解决。

GOBIN 则是强制替代工作空间的 bin 目录,作为 go install 目标保存路径。这可以避免将所有工作空间的 bin 路径添加到 PATH PATH 环境变量中。

在使用 Git 等版本管理工具时,建议忽略 pkg、bin 目录,直接在 src 或具体的子包下创建代码库。

导入包

使用标准库或第三方包前,必须使用 import 导入。参数是以工作空间中以 src 为起始的绝对路径。编译器从标准库开始搜索,然后依次搜索 GOPATH 列表中的各个工作空间

除了使用默认包名,还可以使用别名以解决同名冲突问题。import 导入参数是路径,而不是包名,尽管习惯将包和目录名保持一致,但并不是强制的。在代码中引用包成员时,使用包名而非目录名

有 4 种导入包的方式:

  • import “github.com/fuchencong/test”,默认方式:test.A
  • import X “github.com/fuchencong/test”,别名方式:X.A
  • import . “github.com/fuchencong/test”,简便方式:A,不推荐在正式项目代码中使用
  • import _ “github.com/fuchencong/test”,初始化方式:无法引用,仅用于来初始化目标包

不能直接或间接导入自己,不支持任何形式的循环的导入。未使用的导入会被编译器视为错误。

除了工作空间和绝对路径外,部分工具支持相对路径。可以在非工作空间目录下,直接运行、编译一些测试代码。只要路径准确,就可以使用 go build/run/test 进行编译、运行或测试。但是因为缺少工作空间相关目录,go install 无法工作。在设置了 GOPATH 的工作空间中,相对路径会导致编译失败。

即便将代码托管在 github,有时也希望使用自有域名定义下载和导入路径。方法很简单,在 Web 服务器对应路径中返回包含 go-import 跳转信息即可。此时该包就有两个下载路径,本地也有可能存在两个副本。为避免版本不一致的情况发生,可以添加 import comment 让编译器检查导入路径是否与该注释一致。

组织结构

包(package)是由一个或多个保存在同一个目录下(不含子目录)的源码文件组成。包的作用类似于名字空间,是成员作用域和访问权限的边界。

包名通常使用单数形式,源文件必须使用 UTF-8 格式,否则会导致编译出错。同一目录下所有的源码文件必须使用相同包名称。因为导入时使用绝对路径,所以在搜索路径下,包必须有唯一路径,但无需是唯一名字。

使用 go list 显示包路径列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ go list net/...
net
net/http
net/http/cgi
net/http/cookiejar
net/http/fcgi
net/http/httptest
net/http/httptrace
net/http/httputil
net/http/internal
net/http/pprof
net/internal/socktest
net/mail
net/rpc
net/rpc/jsonrpc
net/smtp
net/textproto
net/url

另外有几个被保留、有特殊含义的包名称:

  • main:可执行入口
  • all:标准库以及 GOPATH 中能找到的所有包
  • std,cmd:标准库以及工具链
  • documentation:存储文档信息,无法导入

所有成员在包内均可以访问,无论其是否在同一源码文件中。但是只有名称首字母大写的为可导出成员,在包外可见。该规则适合全局变量、全局常量、类型、结构字段、函数、方法等。可以通过指针转换等方式绕开该限制。

包内每个源码文件都可以定义一个或多个初始化函数,但是编译器不保证执行次序。实际上这些初始化函数都是由编译器自动生成的一个包装函数进行调用,因此可以保证在单一线程上执行,且仅执行一次。

编译器确保首先完成所有全局变量的初始化,然后才开始执行初始化函数。直到这些全部结束后,运行时才正式进入 main.main 入口函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

var x = 100

func init() {
println("init: ", x)
x++
}

func main() {
fmt.Println("main: ", x)
}
1
2
3
$ ./init
init: 100
main: 101

可以在初始化函数中创建 goroutine,并可以等到它执行结束。如果在多个初始化函数中引用全局变量,那么最好在变量定义处直接赋值,因为无法保证执行次序,所以任何初始化函数中的赋值都有可能由于延迟而造成程序运行不符合预期。

需要注意,初始化函数无法直接调用:

1
2
3
4
5
6
7
8
9
package main

func init() {
println("init")
}

func main() {
init()
}
1
2
3
$ go build call_init.go
# command-line-arguments
./call_init.go:8:2: undefined: init

内部包

在进行代码重构时,通常会把一些内部模块陆续分离出来,以独立包的形式维护,此时基于首字母大小写的访问权限控制机制就显得比较粗犷。因为我们希望这些成员仅在特定范围内访问,而不是向所有用户公开。

内部包机制相当于增加了新的访问机制:所有保存在 internal 目录下的包(包括自身)仅能被其父目录下(含所有层次的子目录)的包访问。导入内部包必须使用完整路径。

依赖管理

如何管理和保存第三方包,一直存在争议。将项目的所有第三方依赖都放在一个独立的工作空间,可能会导致版本冲突。但是放到项目工作空间,又会把工作目录搞乱。为此引入了 vendor 机制,专门存放第三方的包,实现将源码和依赖完整打包分发。

导入 vendor 中的第三方包时,参数是以 vendor 为起点的绝对路径,这就避免了 vendor 目录位置带来的麻烦,这使得导入无论使用 vendor,还是 GOPATH 都能保持一致

引入的第三方包也有可能有自己的 vendor 依赖目录,此时匹配规则是:从当前源文件所在目录开始,逐级向上构建 vendor 全路径,直到发现路径匹配的目标为止。匹配失败,则依旧搜索 GOPATH。另外,vendor 比标准库优先级跟高。

要使用 vendor 机制,必须开始 GO15VENDOREXPERIMENT=1 环境变量开关,且必须是设置了 GOPATH 的工作空间。

反射

反射让我们能在运行期探知对象的类型信息和内存结构,这从一定程度上弥补了静态语言在动态行为上的不足,同时反射还是实现元编程的重要手段。

类型

和 C 数据结构一样,Go 对象头部并没有类型指针,通过其自身是无法在运行期获知任何类型相关信息的。反射操作所需的全部信息都源自接口变量,接口变量除了存储自身类型外,还会保存实际对象的数据类型。如下两个反射入口函数,会将任何传入的对象转换为接口类型:

1
2
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

在面对类型时,需要区分 Type 和 Kind。前者表示真实的类型(静态类型),后者表示其基础结构(底层类型)类别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"reflect"
)

type X int

func main() {
var a X = 10

t := reflect.TypeOf(a)
fmt.Println(t.Name(), t.Kind())
}
1
2
$ ./type
X int

所以在类型判断上,需要选择正确的方式。除了通过实际对象获取类型外,还可以直接构造一些基础复合类型,例如通过 reflect.ArrayOf 构造数组,通过 reflect.MapOf 构造 map。

另外,传入对象应区分基类型和指针类型,因为它们并不属于同一类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"reflect"
)

func main() {
x := 100

tx, tp := reflect.TypeOf(x), reflect.TypeOf(&x)
fmt.Println(tx, tp, tx == tp)
fmt.Println(tx.Kind(), tp.Kind())
fmt.Println(tx == tp.Elem())
}
1
2
3
4
$ ./type_pointer
int *int false
int ptr
true

这里使用方法 Elem 返回指针、数组、切片、字典(值)或通道的基类型。只有在获取结构体指针的基类型后,才能遍历它的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"reflect"
)

type user struct {
name string
age int
}

type manager struct {
user
title string
}

func main() {
var m manager

t := reflect.TypeOf(&m)

if t.Kind() == reflect.Ptr {
t = t.Elem()
}

for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Println(f.Name, f.Type, f.Offset)

if f.Anonymous {
for x := 0; x < f.Type.NumField(); x++ {
af := f.Type.Field(x)
fmt.Println(" ", af.Name, af.Type)
}
}
}
}
1
2
3
4
5
$ ./struct
user main.user 0
name string
age int
title string 24

对于匿名字段,可以用多级索引(按定义顺序)直接访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"reflect"
)

type user struct {
name string
age int
}

type manager struct {
user
title string
}

func main() {
var m manager

t := reflect.TypeOf(m)

name, _ := t.FieldByName("name")
fmt.Println(name.Name, name.Type)

age := t.FieldByIndex([]int{0, 1})
fmt.Println(age.Name, age.Type)
}

同样地,输出方法集时,一样区分基类型和指针类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"reflect"
)

type A int

type B struct {
A
}

func (A) Av() string { return "Av" }

func (*A) Ap() string { return "Ap" }

func (B) Bv() string { return "Bv" }

func (*B) Bp() string { return "Bp" }

func main() {
var b B

t := reflect.TypeOf(&b)
s := []reflect.Type{t, t.Elem()}

for _, t := range s {
fmt.Println(t, ":")

for i := 0; i < t.NumMethod(); i++ {
fmt.Println(" ", t.Method(i))
}
}
}
1
2
3
4
5
6
7
8
*main.B :
{Ap func(*main.B) string <func(*main.B) string Value> 0}
{Av func(*main.B) string <func(*main.B) string Value> 1}
{Bp func(*main.B) string <func(*main.B) string Value> 2}
{Bv func(*main.B) string <func(*main.B) string Value> 3}
main.B :
{Av func(main.B) string <func(main.B) string Value> 0}
{Bv func(main.B) string <func(main.B) string Value> 1}

反射能够探知当前包或外包的非导出结构。还可以利用反射提取 struct tag,还能自动分解。常用于 ORM 映射,或数据格式验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"reflect"
)

type user struct {
name string `field:"name" type:"varchar(50)"`
age int `field:"age" type:"int"`
}

func main() {
var u user
t := reflect.TypeOf(u)

for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("%s: %s %s\n", f.Name, f.Tag.Get("field"), f.Tag.Get("type"))
}
}
1
2
3
$ ./struct_tag
name: name varchar(50)
age: age int

辅助判断方法 Implements、ConvertibleTo、AssignableTo 都是运行期进行动态调用和赋值所必须的。

和 Type 获取类型信息不同,Value 专注于对象实例数据读写。ValueOf 接受一个接口变量作为参数。之前说过,接口变量会复制对象,并且是 unaddressable 的,所以要修改目标对象,就必须使用指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"reflect"
)

func main() {
a := 100
va, vp := reflect.ValueOf(a), reflect.ValueOf(&a).Elem()

fmt.Println(va.CanAddr(), va.CanSet())
fmt.Println(vp.CanAddr(), vp.CanSet())
}
1
2
3
$ ./value
false false
true true

就算传入指针,也需要通过 Elem 获取目标对象,因为被接口存储的指针本身是不能寻址和进行设置操作的。注意,不能对非导出字段直接进行设置操作,无论是当前包还是外包。

注意,不能对非导出字段直接进行设置操作,无论是当前包还是外包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"reflect"
"unsafe"
)

type User struct {
Name string
code int
}

func main() {
p := new(User)
v := reflect.ValueOf(p).Elem()

name := v.FieldByName("Name")
code := v.FieldByName("code")

fmt.Printf("name: canaddr = %v, canset = %v\n", name.CanAddr(), name.CanSet())
fmt.Printf("code: canaddr = %v, canset = %v\n", code.CanAddr(), code.CanSet())

if name.CanSet() {
name.SetString("Tom")
}

if code.CanAddr() {
*(*int)(unsafe.Pointer(code.UnsafeAddr())) = 100
}

fmt.Printf("%v\n", *p)
}
1
2
3
4
./not_export
name: canaddr = true, canset = true
code: canaddr = true, canset = false
{Tom 100}

可以通过 Interface 方法进行类型推断和转换。当然也可以直接用 Value.Int、Value.Bool 等方法进行类型转换,但是失败时会引发 panic,且不支持 ok-idiom。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"reflect"
"fmt"
)

func main() {
type user struct {
Name string
Age int
}

u := user {
"test",
20,
}

v := reflect.ValueOf(&u)
if !v.CanInterface() {
fmt.Println("CanInterface fail")
return
}

p, ok := v.Interface().(*user)
if !ok {
fmt.Println("Interface fail")
return
}

p.Age++
fmt.Printf("%v\n", u)
return
}

接口有两种 nil 状态(接口真实为 nil/接口中的类型信息不为 nil,但是保存的数据为 nil)。解决方法是用 IsNil 判断值是否为 nil。也可以用 unsafe 转换后直接判断 iface.data 是否为零值。

方法

动态调用方法,需要按照 In 列表准备好所需参数。对于变参来说,用 CallSlice 更方便一些,此时传入一个 []interface{} 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"reflect"
)

type X struct{}

func (X) Test(x, y int) (int, error) {
return x + y, fmt.Errorf("err: %d", x + y)
}

func main() {
var a X
v := reflect.ValueOf(&a)
m := v.MethodByName("Test")

in := []reflect.Value{
reflect.ValueOf(1),
reflect.ValueOf(2),
}

out := m.Call(in)
for _, v := range out {
fmt.Println(v)
}
}

无法调用非导出方法,甚至无法获取有效地址。

构建

反射库提供了内置函数 make 和 new 的对应操作。这里介绍一下 MakeFunc,使用它可以实现通用模板,适应不同数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"reflect"
"fmt"
"strings"
)

func add(args []reflect.Value) (results []reflect.Value) {
if len(args) == 0 {
return nil
}

var ret reflect.Value
switch args[0].Kind() {
case reflect.Int:
n := 0
for _, a := range args {
n += int(a.Int())
}
ret = reflect.ValueOf(n)
case reflect.String:
ss := make([]string, 0, len(args))
for _, s := range args {
ss = append(ss, s.String())
}
ret = reflect.ValueOf(strings.Join(ss, ""))
}

results = append(results, ret)
return
}


func makeAdd(fptr interface{}) {
fn := reflect.ValueOf(fptr).Elem()
v := reflect.MakeFunc(fn.Type(), add)
fn.Set(v)
}

func main() {
var intAdd func(x, y int) int
var strAdd func(a, b string) string
makeAdd(&intAdd)
makeAdd(&strAdd)
fmt.Println(intAdd(100, 200))
fmt.Println(strAdd("hello", "world!"))
}

如果语言能够原生支持泛型(例如 C++),就不需要按照上面这种方式来实现了。

性能

反射带来方便的同时,也会造成性能损失。如下是一个简单的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"reflect"
)

type Data struct {
X int
}

var d = new(Data)

func set(x int) {
d.X = x
}

func rset(x int) {
v := reflect.ValueOf(d).Elem()
f := v.FieldByName("X")
f.Set(reflect.ValueOf(x))
}

func main() {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "testing"

func BenchmarkSet(b *testing.B) {
for i := 0; i < b.N; i++ {
set(100)
}
}

func BenchmarkRSet(b *testing.B) {
for i := 0; i < b.N; i++ {
rset(100)
}
}

测试结果:

1
2
3
4
5
6
7
8
$ go test -run None -bench . -benchmem
goos: darwin
goarch: amd64
pkg: bench
BenchmarkSet-8 1000000000 0.611 ns/op 0 B/op 0 allocs/op
BenchmarkRSet-8 14663288 84.1 ns/op 8 B/op 1 allocs/op
PASS
ok bench 2.950s

通过对比,性能差距还是比较大的,因此如果对性能要求较高,还是谨慎使用反射。