中文文档:https://studygolang.com/pkgdoc
基础语法,关键字的使用就没必要赘述了,更多的是记录一些容易记混或者忘记的关键技术点!
个人学习代码记录:https://gitee.com/jiguiquan/gostudy
一、第一部分
1、init const var main 执行顺序:
这几个关键字的执行顺序,对分析Golang语言执行至关重要!有一张很经典的图:
-
单个包中:import —> const —> var —> init,结束初始化;
-
多个包中:如果一个包导入了其他包,那么先完成被导入包的初始化工作!
-
const:常量;
-
var:全局变量(不包含方法体中的局部变量);
-
init:初始化方法;
-
main:入口方法,一般一个程序,只有一个main方法(普通func方法,只会在被调用时触发);
二、在reflect反射场景下,我们何时应该传入值,何时应该传入指针?
反射这块内容很多,尤其在值传递和指针传递这个知识点上,与java很不一样,所以我单独提出来成一个章节!
对于网上很多帖子讲的“获取不到小写字母开头的属性或者方法”:那个算是GoLang语言的几个基础约定了,小写是private,大写是public对外暴露的,没啥好讲的!
1、什么时候需要使用Elem(),什么时候不需要使用?
我们先准备一个结构体Book,它包含3个属性,2个方法(1个指针方法,一个值方法):
type Book struct { Id int Name string Auth string } func (this Book) CallValue() { fmt.Println("Book.CallValue()值调用...") } func (this *Book) CallPoint() { fmt.Println("Book.CallValue()指针调用...") }
我们先不使用Elem()方法,就是简单的答应看看 reflect.typeOf() 在调用指针和非指针时,到底有什么本质区别:
func main() { book := Book{1, "三体", "刘慈欣"} valueType := reflect.TypeOf(book) pointType := reflect.TypeOf(&book) fmt.Printf("一:valueType.Name = %s, valueType.Kind = %s\n", valueType.Name(), valueType.Kind()) fmt.Printf("二:pointType.Name = %s, pointType.Kind = %s\n", pointType.Name(), pointType.Kind()) fmt.Printf("三:pointType.Elem.Name = %s, pointType.Elem.Kind = %s\n", pointType.Elem().Name(), pointType.Elem().Kind()) }
输出结果:
一:valueType.Name = Book, valueType.Kind = struct 二:pointType.Name = , pointType.Kind = ptr 三:pointType.Elem.Name = Book, pointType.Elem.Kind = struct
我们可以得出结论:
当我们在对指针参数进行反射时,直接得到的是一个ptr(pointer)类型的数据,而不是它背后真正的struct结构体!(对比一&二)
我们我们想获得它背后真正的结构体,就需要使用到 Elem() 方法了,当调用了Elem()方法后,就和直接值参数一样了!(对比一&三)
既然到这里了,那我们就简单地跟踪一下源码吧:
// C:/Program Files/Go/src/reflect/type.go:897 // 显然,Elem方法会先获得真正的Kind类型,进行分支判断:如果是Ptr指针的话,那么需要获得 tt.elem ,并返回 func (t *rtype) Elem() Type { switch t.Kind() { case Array: tt := (*arrayType)(unsafe.Pointer(t)) return toType(tt.elem) case Chan: tt := (*chanType)(unsafe.Pointer(t)) return toType(tt.elem) case Map: tt := (*mapType)(unsafe.Pointer(t)) return toType(tt.elem) case Ptr: tt := (*ptrType)(unsafe.Pointer(t)) return toType(tt.elem) case Slice: tt := (*sliceType)(unsafe.Pointer(t)) return toType(tt.elem) } panic("reflect: Elem of invalid type " + t.String()) }
需要弄清楚 tt.elem 具体是啥?
// 中文翻译就是:指针指向的实际element的type // ptrType represents a pointer type. type ptrType struct { rtype elem *rtype // pointer element (pointed at) type // 指针指向的实际element的type }
-
破案了,很清晰,Elem() 方法:判断出是指针后,返回指针指向的实际element元素的type类型;
-
ValueOf(&book).Elem() 原理相同,可以自行跟踪下源码即可!
所以,咱们最终的结论是:
当反射入参为指针时,我们想获得指针背后实际element的Type或者Value时,就需要先调用Elem()方法进行一次转化!
先这么简单理解吧!
2、指针类型获得属性数量及属性列表:
还是使用上面的Book结构体,先来个错误的写法,获取属性数量:
func main() { book := Book{1, "三体", "刘慈欣"} valueType := reflect.TypeOf(book) pointType := reflect.TypeOf(&book) valueNumField := valueType.NumField() pointNumField := pointType.NumField() fmt.Printf("valueType.NumField = %d", valueNumField) fmt.Printf("pointType.NumField = %d", pointNumField) }
此方法会直接报错: panic: reflect: NumField of non-struct type *main.Book
显然,non-struct type 代表:指针不是一个结构体type,所以不允许调用NumField!
简单修改下,使用Elem就没有问题了,就不贴代码了,和后面的案例一起体现:
func main() { book := Book{1, "三体", "刘慈欣"} pointType := reflect.TypeOf(&book) pointValue := reflect.ValueOf(&book) numField1 := pointType.Elem().NumField() numField2 := pointValue.Elem().NumField() fmt.Printf("valueType.NumField = %d\n", numField1) fmt.Printf("pointValue.NumField = %d\n", numField2) for i := 0; i < numField1; i++ { field := pointType.Elem().Field(i) value := pointValue.Elem().Field(i).Interface() fmt.Printf("属性:%s: %v = %v\n", field.Name, field.Type, value) } }
执行结果如下:
valueType.NumField = 3 pointValue.NumField = 3 属性:Id: int = 1 属性:Name: string = 三体 属性:Auth: string = 刘慈欣
由此,我们可以得出结论:
在想获得属性个数时,我们既可以使用 TypeOf() 也可以使用 ValueOf();
当想获得结构体属性的列表时,我们需要用到 TypeOf() 的返回结果,但是如果想获得具体的值,就需要用到 ValueOf() 的返回值!
其实,反射获取属性列表还是比较容易处理的,一般不会出现什么错误,但是当获取方法列表的时候,可能就会遇到错误了!
3、对比反射调用对象方法时,值入参和指针入参的区别:
还是使用上面的Book结构体,先看一个简单的案例:
func main() { book := Book{1, "三体", "刘慈欣"} valueType := reflect.TypeOf(book) pointType := reflect.TypeOf(&book) numMethod1 := valueType.NumMethod() numMethod2 := pointType.NumMethod() fmt.Printf("valueType.NumField = %d\n", numMethod1) fmt.Printf("pointType.NumField = %d\n", numMethod2) for i := 0; i < numMethod1; i++ { method := valueType.Method(i) fmt.Printf("valueType方法:%s:%s\n", method.Name, method.Type) } for i := 0; i < numMethod2; i++ { method := pointType.Method(i) fmt.Printf("pointType方法:%s:%s\n", method.Name, method.Type) } }
执行的结果如下:
valueType.NumField = 1 pointType.NumField = 2 valueType方法:CallValue:func(main.Book) pointType方法:CallPoint:func(*main.Book) pointType方法:CallValue:func(*main.Book)
显然:
通过实际值传参,通过反射时,我们是没有办法获取到指针类型的方法的;
而通过指针传参,通过反射时,我们就可以获取到所有的方法;
所以,如果我们想通过获取全部的方法时,就一定要使用指针传参!
那如果我们想调用Book中的方法呢?
func main() { book := Book{1, "三体", "刘慈欣"} pointType := reflect.TypeOf(&book) pointValue := reflect.ValueOf(&book) numMethod := pointType.NumMethod() for i := 0; i < numMethod; i++ { method := pointValue.Method(i) // 因为我们的方法没有入参,所以我就定义了一个空的params,如果有入参,就要通过make开辟空间,放入参数了 var params []reflect.Value method.Call(params) } }
执行结果如下:
Book.CallValue()指针调用... Book.CallValue()值调用...
调用没有问题!
正常工作中肯定不可能是像上面那样遍历调用所有的方法,而是通过 .MethodByName("") 得到具体的 method 后,传入具体的参数列表,进行调用!
三、为什么要用Go Modules取代GOPATH 工作模式
1、何为GOPATH工作模式?
首先我们可以使用 go env查看或设置当前的环境变量:
# 查看环境变量 C:\Users\jiguiquan> go env .... set GOPATH=D:\GOPATH set GOPROXY=https://goproxy.cn set GOROOT=C:\Program Files\Go ... ## 设置环境变量 C:\Users\jiguiquan> go env -w GOPROXY=https://goproxy.cn
可以看到我们设置的GOPATH路径!进入到这个目录下,我们可以看到3个经典子目录:
go ├── bin # 存储所编译生成的二进制文件(执行go build后生成的二进制文件) ├── pkg # 存储预编译的目标文件,以加快程序的后续编译速度 └── src # 存储所有.go文件或源代码
在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。
2、GOPATH模式的弊端:
在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有以下弊端:
-
无版本控制概念:在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本;
-
无法同步一致第三方版本号:在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致;
-
无法指定当前项目引用的第三方版本号:你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar,本地安装的是什么版本,项目引用的就是什么版本,完全不由项目自己决定;
3、Go Modules 模式的引入——常用的命令:
命令 | 作用 |
go mod init | 生成 go.mod 文件 |
go mod download | 下载 go.mod 文件中指明的所有依赖 |
go mod tidy | 整理现有的依赖 |
go mod graph | 查看现有的依赖结构 |
go mod edit | 编辑 go.mod 文件 |
go mod vendor | 导出项目所有的依赖到vendor目录 |
go mod verify | 校验一个模块是否被篡改过 |
go mod why | 查看为什么需要依赖某模块 |
4、Go Modules 模式重要的环境变量:
以下环境变量对Go Modules模式很重要:
GO111MODULE="auto" GOPROXY="https://goproxy.cn" GONOPROXY="" GOSUMDB="sum.golang.org" GONOSUMDB="" GOPRIVATE=""
-
GO111MODULE
Go语言提供了 GO111MODULE 这个环境变量来作为 Go modules 的开关,其允许设置以下参数:
-
auto:只要项目包含了 go.mod 文件的话启用 Go modules(默认值);
-
on:启用 Go modules,推荐设置,将会是未来版本中的默认值;
-
off:禁用 Go modules,不推荐设置。
# 我们建议手动将该值设为on go env -w GO111MODULE=on
GOPROXY
这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取,一般都会将其设置为国内的代理地址:
-
阿里云:https://mirrors.aliyun.com/goproxy/
-
七牛云:https://goproxy.cn,direct
# 最后的direct为特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等); ## 当值列表中上一个 Go module proxy 返回 404 或 410 错误时,Go 自动尝试列表中的下一个; ### 遇见 “direct” 时回源,遇见 EOF 时终止并抛出类似“invalid version: unknown revision…”的错误; go env -w GOPROXY=https://goproxy.cn,direct
GOSUMDB
它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止;
GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database);
因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心;
GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败;
更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。
而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE,如:
$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote" # 设置后,前缀为 git.example.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块如果不想每次都重新设置,我们也可以利用通配符,例如:
$ go env -w GOPRIVATE="*.example.com" # 设置后,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database; ## 需要注意的是不包括 example.com 本身。
5、使用Go Modules模式初始化我们的项目: