文章目录
- 包和依赖管理
- 依赖管理
- go mod
- go get
- go.mod 文件
- go.sum 文件
- Go Modules 发布包
- 接口
- 空接口
- 接口值
- 类型断言
- 反射
- reflect.TypeOf
- reflect.ValueOf
- 结构体反射
项目代码地址:04-PackageInterfaceReflection
包和依赖管理
Go 使用包来支持代码模块化和代码复用,一个包由多个 .go
文件组成。
基本格式:package packagename
,其中 packagename 表示包名
- 一个文件夹直接包含的文件只能属于一个包
- 包名为
main
的包是程序入口包,仅该包编译后会得到一个可执行文件 - 首字母大/小写控制标识符是否对外可见/不可见,大写则对包外可见
- 引入的包都会被编译到可执行文件中
包的引入
- 禁止循环导入包
- 导入的包必须都有使用
- 三种常见的引入包模式,
.
、_
、别名
每个包在初始化时都会先执行依赖包中声明的 init
函数,再执行当前包中声明的 init
函数。
import (_ "00-LocalPackage" // 匿名Calc "04-PackageInterfaceReflection/calc" // 别名. "fmt" // 不带前缀,不建议使用,容易重名
)func function01() {Printf("%s\n", "不带前缀")Println(Calc.Add(1, 2))
}
date 包初始化ing~
不带前缀
3
- 匿名导入包会先执行其
init
函数 - 使用
.
导入包,则不需要使用该包名作为前缀调用,Printf
、Println
- 使用别名
Calc
导入包,调用该包下的函数需要使用别名
上述示例中,00-LocalPackage
包是本地没被发布到其他任何代码仓库的本地包,04-PackageInterfaceReflection/calc
则是当前项目下的包,结构如下:
- 04-PackageInterfaceReflection|- calc|- add.go|- go.mod|- main.go
- 00-LocalPackage|- go.mod|- date.go
当前项目内的其他包,使用很简单,如上述直接 import 项目名/包名
导入即可。
本地包导入如下,可以在 go.mod
文件中使用 replace
语句将依赖临时替换为本地包的相对路径:
04-PackageInterfaceReflection/go.mod
:
require 00-LocalPackage v0.0.0
replace 00-LocalPackage => ../00-LocalPackage
依赖管理
Go 1.16 版本默认开启 Go Modules 管理依赖
GOPROXY
设置 Go 模块代理,使 Go 在后续拉取模块版本时脱离传统 vcs 方式,直接通过镜像站点快速拉取。
go env -w GOPROXY=https://goproxy.cn,direct
GOPRIVATE
设置了 GOPROXY 后,go 命令会从配置的代理地址拉取和校验依赖包。当项目中引入非公开的包(私有仓库或公司内部 git 仓库),就无法正常从代理拉取,需要配置 GOPRIVATE。
GOPRIVATE 用来告诉 go 命令哪些仓库是私有的,不必通过代理服务器拉取和校验。
go env -w GOPRIVATE=gitee.com
这样就可以正常拉取 gitee.com
为前缀的依赖包了
go mod
常用的 go mod
命令:
指令 | 介绍 |
---|---|
go mod download | 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录) |
go mod edit | 编辑go.mod文件 |
go mod graph | 打印模块依赖图 |
go mod init | 初始化当前文件夹, 创建go.mod文件 |
go mod tidy | 增加缺少的module,删除无用的module |
go mod vendor | 将依赖复制到vendor下 |
go mod verify | 校验依赖 |
go mod why | 解释为什么需要依赖 |
我们在代码中删除依赖代码后,相关的依赖库并不会在 go.mod
文件中自动移除。这种情况下我们可以使用 go mod tidy
命令更新 go.mod
中的依赖关系。
go get
在项目中执行 go get
命令可以下载依赖包,并且还可以指定下载的版本。
- 运行
go get -u
将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号) - 运行
go get -u=patch
将会升级到最新的修订版本 - 运行
go get package@version
将会升级到指定的版本号 version
go.mod 文件
go.mod
文件记录了当前项目中所有依赖包的相关信息
声明依赖的基本格式:require module/path v1.2.3
-
require: 声明依赖关键字
-
module/path: 依赖包的引入路径
-
v1.2.3: 依赖包的版本号
-
latest: 最新版本
-
v1.2.3: 详细版本号
- 主版本号:发布了不兼容的版本迭代时递增
- 次版本号:发布了功能性更新时递增
- 修订号:发布了 bug 修复类更新时递增
-
commit hash: 指定某次 commit hash
-
go.sum 文件
在 Go Modules 下载依赖后生成,记录依赖包及其 hash 值。Go 采用分布式方式管理包,为了防止依赖包被非法篡改,Go Modules 引入了 go.sum
机制对依赖包进行校验。
基本格式:
<module> <version> <hash>
<module> <version>/go.mod <hash>
Go Modules 发布包
-
gitee.com
创建仓库,并下载到本地git clone git@gitee.com:Cauchy_AQ/hello.git
-
hello 项目目录下初始化,设置项目路径
go mod init gitee.com/Cauchy_AQ/hello
-
创建 hello.go 文件,提供方法 SayHello()
-
package helloimport "fmt"func SayHello() {fmt.Println("你好,我是Cauchy!")}
-
-
项目代码 push 到远端 master 分支
git add .
git commit -m "SayHello() v0.1.0"
git push origin master
-
为代码包打上标签
git tag -a v0.1.0 -m "release version v0.1.0"
-
项目代码 push 到远端标签分支
git push origin v0.1.0
-
迭代版本
v2.0.0
-
func SayHello(name string) {fmt.Printf("你好%s,我是Cauchy!", name)}
-
-
go.mod 修改当前包的引入路径,添加后缀
v2
module gitee.com/Cauchy_AQ/hello/v2
-
发布新的主版本
git add .
git commit -m "SayHello(string) v2.0.0"
git push
git tag -a v2.0.0 -m "release version v2.0.0"
git push origin v2.0.0
-
项目使用自己发布的包
-
设置 GOPRIVATE
go env -v GOPRIVATE=gitee.com
-
项目导入包
go get gitee.com/Cauchy_AQ/hello/v2@v2.0.0
go mod tidy
-
执行完上述操作后,就成功发布包在 gitee 平台,并且作为第三方库,可以随时获取使用。
import (hello "gitee.com/Cauchy_AQ/hello"helloV2 "gitee.com/Cauchy_AQ/hello/v2"
)func function02() {hello.SayHello() // 你好,我是Cauchy!helloV2.SayHello("AQ") // 你好AQ,我是Cauchy!
}
需要注意:
从 v2 版本开始,主要版本必须出现在模块路径末尾(…/v2)。
接口
Go 1.18 版本开始,接口分为 一般接口 和 基础接口,在此讨论基本接口,后续谈及泛型在讨论一般接口。
一个接口类型的定义中只包含方法,称为基本接口。
基本格式:
type 接口类型名 interface {方法名(参数列表) 返回值列表
}
实现接口
如下所示:
- 类型 Dog 实现了接口 Animal 的 Move、Say 方法,即 Dog 实现了接口 Animal
- 接口 FlyAnimal 包含了接口 Animal 所有的方法,即 FlyAnimal 实现了接口 Animal
// Interface
type Animal interface {Move()Say()
}//
type FlyAnimal interface {AnimalFly()
}type Dog struct{}func (d Dog) Move() {Println("dog move")
}func (d Dog) Say() {Println("dog say")
}func function03() {var a Animal // 接口类型变量var d Dog = Dog{}a = d // 能存储所有实现了该接口的类型变量a.Move() // dog movea.Say() // dog say
}
Go 有对指针求值的语法糖:
Dog 是值接收者,赋值给接口类型变量 a 时,无论是值还是指针都可以直接赋值。Cat 是指针接收者,赋值给 a 时,只能赋值地址。
- 值接收者实现的接口,可用值类型和指针类型
- 指针接收者实现的接口,只能用指针类型
var d *Dog = &Dog{}
a = d
var c Cat = Cat{}
a = &c
type Dog struct{}func (d Dog) Move() {Println("dog move")
}func (d Dog) Say() {Println("dog say")
}type Cat struct{}func (c *Cat) Move() {Println("cat move")
}func (c *Cat) Say() {Println("cat say")
}
只要类型实现了接口的所有方法,那么该类型就实现了接口
- 多种类型实现同一接口
type HomeWork interface {DoA()DoB()
}type Week1 struct{}func (w Week1) DoA() {}type Week2 struct {Week1
}func (w Week2) DoB() {}
- 接口组合
type A interface{PlayA()
}
type B interface{PlayB()
}type AB interface {AB
}
空接口
空接口指没有定义任何方法的接口类型,因此任何类型都可以视为实现了空接口。所以空接口的类型变量可以存储任意类型的值。
func function04() {a := make(map[int]interface{})a[1] = [...]int{1, 2, 3}a[2] = "Golang"a[3] = 123for k, v := range a {Println(k, v)}/*2 Golang3 1231 [1 2 3]*/
}
Go 中只要是可比较类型都可以作为 key ,除了 slice ,map,function 这几种类型,其他类型都可以作为 map 的 key。因此 interface{}
空接口类型也不可以作为 key。
Go 中的 any
类型实际就是空接口类型:type any = interface{}
func function05() {var a interface{}a = []int{1, 2, 3}Println(a) // [1 2 3]var b anyb = []int{0}Println(b) // [0]func(a interface{}) {switch a.(type) {case int:Println("int")case string:Println("string")default:Println("None")}}("123") // string
}
接口值
接口值由类型(type)和值(value)组成,这两部分根据存入值不同而发生变化,也称之为接口的动态类型和动态值。
// 接口值
func function06() {var c AnimalPrintln(c == nil) // truec = new(Cat)Println(c == nil) // falsePrintf("%T\n", c) // *main.Cat
}
类型断言
接口值可能被赋为任意类型的值,通过类型断言从接口值获取其存储的具体数据类型。
基本语法:x.(Type)
,返回两个参数:第一个是转化为 Type 类型的变量;第二个是一个 bool 值,true 表示断言成功。
// 类型断言
func function07() {var x interface{}x = []int{1, 2, 3}switch v := x.(type) {case int:Println("int", v)case string:Println("string", v)case []int:Println("[]int", v) // []int [1 2 3]default:Println("failed")}
}
tips
下述代码可以在程序编译阶段验证某一结构是否满足特定的接口类型
type IRouter interface{...}
type RouterGroup struct {...}
var _ IRouter = (*RouterGroup)(nil) // 确保 RouterGroup 实现了接口 IRouter
反射
上文通过类型断言,获取空接口类型的动态值和类型。那么也可以通过反射,在程序运行时动态的获取一个变量的类型信息和值信息。
Go 语言反射相关功能由内置的 reflect
包提供,任何接口值在反射中都可以理解为由 reflect.Type
和 reflect.Value
两部分组成。分别由 reflect.TypeOf
和 reflect.ValueOf
函数来获取任意值的 Type
和 Value
信息。
reflect.TypeOf
函数签名:func TypeOf(i interface{}) Type
通过反射得到的类型信息分为 Type
和 Kind
两种,
-
Type
指声明的类型,名称由reflect.Type
的Name
方法得到- 数组、切片、Map、指针等类型变量的
Type
名称都是空字符串
- 数组、切片、Map、指针等类型变量的
-
Kind
指语言底层的类型,名称由reflect.Type
的Kind
方法得到- 当需要区分指针、结构体等大品种类型,就用到
Kind
- 当需要区分指针、结构体等大品种类型,就用到
func reflectType(x interface{}) {v := reflect.TypeOf(x)Printf("type:%v kind:%v\n", v.Name(), v.Kind())
}type MyInt intfunc function08() {var a *float32var b MyIntvar c int64reflectType(a) // type: kind:ptrreflectType(b) // type:MyInt kind:intreflectType(c) // type:int64 kind:int64type ABC struct {name string}var d = ABC{"123"}reflectType(d) // type:ABC kind:structe := &struct {ID int}{1010}reflectType(e) // type: kind:ptr
}
reflect.ValueOf
函数签名:func ValueOf(i any) Value
reflect.ValueOf
返回 reflect.Value
类型,它是结构体类型,包含了原始值信息,可以与原始值之间互相转换。
func reflectValue(x interface{}) {v := reflect.ValueOf(x) // reflect.Valuek := v.Kind()switch k {case reflect.Int:Println("Int", v.Int())case reflect.Int32:Println("Int32", int32(v.Int()))case reflect.Float64:Println("Float64", v.Float())default:Println("None")}
}func function09() {var a intvar c int32 = 1b := 0.1reflectValue(a) // Int 0reflectValue(c) // Int32 1reflectValue(b) // Float64 0.1
}
结构体反射
任意值通过 reflect.Typeof()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和 Field()
方法获得结构体成员的详细信息。
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息 |
NumField() int | 返回结构体成员字段数量 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串赌赢的结构体字段信息 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构 |
遍历 Person 结构体,根据 tag 获取对应的名称,并获取对应字段的值:
reflect.Type
、reflect.Value
类型,如果是指针类型(reflect.Ptr
),可以通过Elem
方法获取指针对应的值reflect.Type
的NumField
和Field
获得结构体成员的详细信息(StructField
)reflect.Value
的FieldByName
通过字段名字获取对应的值reflect.Value
的FieldByIndex
通过[]int
切片指定字段的层级获取具体某个字段的值,比如[]int{0, 1}
表示结构体第一个字段内部的第二个字段
type Person struct {Name string `info:"name"`Age int8 `info:"age"`
}func function10() {cauchy := &Person{"cauchy",20,}t := reflect.TypeOf(cauchy)v := reflect.ValueOf(cauchy)if t.Kind() == reflect.Ptr && v.Kind() == reflect.Ptr {Println("reflect.Ptr") // reflect.Ptr}for i := 0; i < t.Elem().NumField(); i++ {f := t.Elem().Field(i)Println(f.Index, f.Name, f.Type)Println(f.Tag.Get("info"), v.Elem().FieldByName(f.Name))} /*[0] Name stringname cauchy[1] Age int8age 20*/value0 := v.Elem().FieldByIndex([]int{0})value1 := v.Elem().FieldByIndex([]int{1})Println(value0, value1) // cauchy 20
}
方法 | 说明 |
---|---|
FieldByNameFunc(match func(string) bool) (StructField, bool) | 根据传入的匹配函数匹配需要的字段 |
NumMethod() int | 返回该类型的方法集中方法的数目 |
Method(int) Method | 返回该类型方法集中的第 i 个方法 |
MethodByName(string) (Method, bool) | 根据方法名返回该类型方法集中的方法 |
获取结构体方法,并进行调用:
-
reflect.Type
的NumMethod
可以获取该类型可导出的方法(首字母大写)- 指针类型(
reflect.Ptr
)可以获取该类型的指针、值接收者的所有方法 - 值类型仅可以获取该类型的值接收者的所有方法
- 指针类型(
-
reflect.Type
的Method
通过下标 index 获取该类型的对应的方法Method
-
reflect.Value
的MethodByName
通过方法名称获取对应可执行方法,实际返回是一个reflect.Value
类型-
返回的类型可以通过
IsValid
和IsNil
判断是否有效,是否为空 -
类型存在,则调用
Call
可以调用该类型实际的对应的方法Call
接收参数是[]reflect.Value
类型
-
func (p *Person) SetName(name string) {p.Name = name
}func (p Person) GetName() string {return p.Name
}func function11() {p := &Person{"cauchy",20,}t := reflect.TypeOf(p)v := reflect.ValueOf(p)Println(t.Kind(), t.Elem().Kind()) // ptr struct// 获取的方法必须是可导出的,首字母大写// 指针类型获取(t.NumMethod()),所有接收者方法// 值类型获取(t.Elem().NumMethod()),值接收者方法ptrN, N := t.NumMethod(), t.Elem().NumMethod()Println(ptrN, N) // 2, 2for i := 0; i < ptrN; i++ {m := t.Method(i)Println(m.Name, m.Type, m.Func)} /*GetName func(*main.Person) string 0x1ddf80SetName func(*main.Person, string) 0x1ddb80*/// 通过名称获取函数f := v.MethodByName("GetName")// 如果函数存在if f.IsValid() && !f.IsNil() {ret := f.Call(nil) // 调用,不用参数 nilPrintln(ret) // [cauchy]}// 参数必须是 []reflect.Value 类型v.MethodByName("SetName").Call([]reflect.Value{reflect.ValueOf("AQ")})ret := v.MethodByName("GetName").Call(nil)Println(ret) // [AQ]
}
上述代码需要注意的一点,要想修改一个反射对象,那么它必须是可设置的。((p *SetName)
)
读者可自行运行上述代码,做相应修改测试是否是预期结果。