Last updated on February 22, 2024 am
Golang基础
参考学习资料:
Golang简介
go语言的优势
部署简单
可以直接编译成机器码可执行
不依赖其他库
直接运行即可部署
静态类型语言
语言层面的并发
强大的标准库
简单易学
25个关键字,内嵌C语法支持
面向对象的特征,能够跨平台
go语言没有异常 ,全部都用ERROR来表示
go应用方向
云计算基础设施建设
Docker, kubernetes
Consul, cloudflare CDN
基础后端软件 :tide, influxdb, cockroachdb
微服务 :go-kit, micro
互联网基础设施 : 以太坊,hyperledger
Go的环境安装
下载官网
go的官网下载网站,选择合适的系统版本进行安装https://go.dev/dl/
安装步骤
下载安装包并按照安装包的指引下载相关的内容
对于Mac系统会直接配置好环境变量,根据官网的安装手册进行安装 https://go.dev/doc/install
测试GO的版本
测试GO的环境变量
GO环境变量
GOROOT路径
GOROOT 表示的是安装包所在的位置,一般不需要修改
GOPATH路径
GOPATH表示的是运行文件所在的位置,表示的是workspace的文件位置,GOPATH是我们的工作空间,保存go项目代码和第三方依赖包GOPATH 可以设置多个,其中,第一个将会是默认的包目录,使用
go get 下载的包都会在第一个path中的src目录下,使用 go
install时,在哪个GOPATH中找到了这个包,就会在哪个GOPATH下的bin目录生成可执行文件
修改GOPATH的路径
1 export GOPATH="/Users/lihaibin/workspace/golang"
将文件查找的路径设置为GOROOT和GOPATH的并集合
1 export PATH=$PATH :$GOROOT /bin:$GOPATH /bin
将两个部分并在一起之后,就能从两个地方开始寻找定义的包
查找文件的路径顺序
首先会从GOROOT进行搜索,接着从GOPATH进行搜索。
GOPATH是开发时的工作目录。用于:
保存编译后的二进制文件。
go get
和go install
命令会下载go代码到GOPATH。
import包时的搜索路径
使用GOPATH时,GO会在以下目录中搜索包:
GOROOT/src
:该目录保存了Go标准库代码。
GOPATH/src
:该目录保存了应用自身的代码和第三方依赖的代码。
GOPATH的弊端
在 GOPATH 的 $GOPATH/src
下进行 .go
文件或源代码的存储,我们可以称其为 GOPATH
的模式,这个模式拥有一些弊端.
无版本控制概念.
在执行go get
的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。
无法同步一致第三方版本号. 在运行 Go
应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。
无法指定当前项目引用的第三方版本号. 你没办法处理
v1、v2、v3 等等不同版本的引用问题,因为 GOPATH
模式下的导入路径都是一样的,都是github.com/foo/bar
。
GOPROXY设置
这个环境变量主要是用于设置 Go 模块代理(Go module
proxy),其作用是用于使 Go
在后续拉取模块版本时直接通过镜像站点来快速拉取 。
GOPROXY
的默认值是:https://proxy.golang.org,direct
,proxy.golang.org
国内访问不了,需要设置国内的代理
阿里云 https://mirrors.aliyun.com/goproxy/
七牛云 https://goproxy.cn,direct
并通过以下的命令进行设置
1 go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY 的值是一个以英文逗号 “,” 分割的 Go
模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为
“off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。
1 go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct
GO111MODULE
GO111MODULE 有三个值:off, on和auto(默认值)。
GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
当前目录在GOPATH/src之外且该目录包含go.mod文件
当前文件在包含go.mod文件的目录下面。
执行以下命令开启go mod管理
1 go env -w GO111MODULE=on
Go mod操作
1 go mod init github.com/hub/project
Go基本语法
如何编译并运行一个Go文件
对于已经写好的go文件,这里以hello.go作为例子,直接使用以下语句进行编译并运行
或者将编译和运行两个过程分开,先编译后运行:
1 2 go build hello.go ./ hello
写一个hello.go
首先给出基本框架
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" , "time" )func main () { fmt.Println("hello world!" ) time.Sleep(1 *time.Second) }
程序的第一行声明了名为main的package。一个package会包含一个或多个.go源代码文件。每一个源文件都是以package开头。 比如我们的例子里是package
main。这行声明语句表示该文件是属于哪一个package。
第一行代码package
main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package
main。package main表示一个可独立执行的程序,每个 Go
应用程序都包含一个名为 main 的包。
下一行import "fmt" 告诉 Go 编译器这个程序需要使用
fmt 包(的函数,或其他元素),fmt 包实现了格式化
IO(输入/输出)的函数
.
和没有.
导入包的区别,如果一开始引入的时候有.
那么就不需要指定哪个包的来调用函数,否则需要再调用函数的时候指定对应的包package
下一行func main()是程序开始执行的函数。main
函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有
init() 函数则会先执行该函数)。
一个程序的main
入口函数必须不带任何输入参数和返回结果。而且go语言的语法,定义函数的时候,‘{’
必须和函数名在同一行,不能另起一行
变量的声明
声明变量的一般形式是使用 var 关键字
第一种声明:
指定变量类型,声明后若不赋值,使用默认值0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var v_name v_type v_name = valuepackage mainimport "fmt" func main () { var a int fmt.Printf(" = %d\n" , a) } $go run test.go a = 0
第二种声明:
根据值自行判定变量类型。
第三种声明:
省略var, 注意
:=左侧的变量不应该是已经声明过的,就是:=只能用于没有被声明的变量赋值上,否则会编译错误
1 2 3 4 5 6 7 v_name := valuevar a int = 10 var b = 10 c : = 10
几种声明类型的对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" func main () { var a int fmt.Printf("a = %d\n" , a) var b int = 10 fmt.Printf("b = %d\n" , b) var c = 20 fmt.Printf("c = %d\n" , c) d := 3.14 fmt.Printf("d = %f\n" , d) }
全局变量声明
和一般的定义变量的方式一样
1 2 3 var x, y int var c, d int = 1 , 2 var e, f = 123 , "lihaibin"
特殊的定义全局变量的方式,而且:=的定义方式不能够用于定义全局变量
1 2 3 4 5 6 7 8 9 10 var ( a int b bool )func main () {}
多变量声明
:=不能用于已经被初始化之后的变量的赋值,如果对于_的情况是不具备可读性,相当于忽略
1 2 3 4 5 6 7 8 9 10 11 func main () { g, h := 123 , "需要在func函数体内实现" fmt.Println(x, y, a, b, c, d, e, f, g, h) _, value := 7 , 5 fmt.Println(value) }
常量
常量的声明方式
常量是一个简单值的标识符,在程序运行时,不会被修改的量 。常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:
1 2 const identifier [type ] = valueconst b string = "abc"
隐式定义类型方法:
多重赋值
1 const a, b, c = 1 , false , "str"
枚举类型
1 2 3 4 5 const ( Unknown = 0 Female = 1 Male = 2 )
常量可以用len(), cap(),
unsafe.Sizeof()常量计算表达式的值。常量表达式中,函数必须是内置函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "unsafe" const ( a = "abc" b = len (a) c = unsafe.Sizeof(a) )func main () { println (a, b, c) }
输出结果为:abc, 3, 16
unsafe.Sizeof(a) = 16
字符串类型在 go 里是个结构,
包含指向底层数组的指针和长度,这两部分每部分都是 8
个字节,所以字符串类型大小为 16 个字节。
常量中的iota标识符
在 golang
中,一个方便的习惯就是使用iota
标示符,简化了常量用于增长数字的定义。
下面的代码中,当第一行赋值了iota之后,那么相当于初始化位置是0,后面的依次增加是1,2
1 2 3 4 5 const ( CategoryBooks = iota CategoryHealth CategoryClothing )
如果对iota
进行运算,其实相当于是选择当前的行作为iota的取值进行运算,如果中间不对运算加以改变,那么会一直持续按照当前的运算规则执行下去
1 2 3 4 5 6 7 8 9 const ( BEIJING = 10 * iota SHANGHAI HANGZHOU )
同样的在同一个const中去定义不同的
iota
的计算方式也可以,iota
的取值就是选择当前的行,从哪个地方开始改变,那么就改成不同的计算方式
1 2 3 4 5 6 7 8 9 const ( a, b = iota + 1 , iota + 2 c, d e, f g, h = iota * 2 , iota * 3 i, k )
以下是输出的内容:
1 a= 1 b= 2 c= 2 d= 3 e= 3 f= 4 g= 6 h= 9 i= 8 k= 12
函数
基本函数的定义
多个返回值初始化设置了函数的形参之后,初始值是0
go每次设置一个变量值之后都有初始值,如果是数据就是0,如果是字符串那么就是空,防止出现一些野指针的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func swap (x, y string ) (string , string ) { return y, x }func main () { a, b := swap("Mahesh" , "Kumar" ) fmt.Println(a, b) }
输出的结果是
import和init
所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,执行main包中的init函数,最后执行main函数。下图详细地解释了整个执行过程:
init的调用顺序
分别建不同的文件夹对应的就是package的名字,相应的在.go文件内部声明package的名字
main 函数只能在package main中
注意:
在包中设置接口的时候,函数名称必须第一个字母是大写,如果是小写的话将无法识别
如果函数名第一个是大写就是对外开放的函数,认为是public
如果函数名第一个是小写的话就认为是私有的函数,认为是private
init函数的调用过程,首先会对包中的init进行初始化再进行调用接口
如果你导入了包比如lib1,但是没有使用这个包里面的接口函数,仍然会报错
以下是一个import包的例子,首先定义两个不同包以及对应的接口函数和初始化函数的实现
1 2 3 4 5 6 7 package InitLib1import "fmt" func init () { fmt.Println("lib1" ) }
1 2 3 4 5 6 7 package InitLib2import "fmt" func init () { fmt.Println("lib2" ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" "GolangTraining/InitLib1" "GolangTraining/InitLib2" )func init () { fmt.Println("libmain init" ) }func main () { fmt.Println("libmian main" ) }
"GolangTraining/InitLib1""GolangTraining/InitLib2"
是两个包的地址,go会默认从GOROOT和GOPATH两个默认的位置进行寻找,首先要保证地址的正确性
代码的输出:
1 2 3 4 lib1 lib2 libmain init libmian main
匿名导包方式
如果我不想调用lib1的函数接口,但是想使用lib1的init()函数怎么办呢,如果这个时候直接导入了包但是不调用接口,就会出现上述的错误
在导入的包前面加上下划线来认为这个包是匿名的,这样就能知进行init操作
1 2 3 4 import ( "fmt" _"lib2" )
那么这个时候就只会调用init()
函数同时不会出错
只调用init()函数
除了能够匿名导包之外,还能给新导入的包起个别的名字,比如叫mylib作为新的别名
或者直接使用·
来进行调用
最好别使用这种,如果两个包的函数名称一样那么可能会导致出现歧义的情况
函数值传递
函数如果使用参数,该变量可称为函数的形参。
形参 就像定义在函数体内的局部变量 。调用函数,可以通过两种方式来传递参数:值传递和指针传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go
语言使用的是值传递 ,即在调用过程中不会影响到实际参数。
1 2 3 4 5 6 7 8 9 10 11 12 func swap (x, y int ) int { var temp int temp = x x = y y = temp return temp; }
接下来,让我们使用值传递来调用 swap() 函数:
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 package mainimport "fmt" func main () { var a int = 100 var b int = 200 fmt.Printf("交换前 a 的值为 : %d\n" , a ) fmt.Printf("交换前 b 的值为 : %d\n" , b ) swap(a, b) fmt.Printf("交换后 a 的值 : %d\n" , a ) fmt.Printf("交换后 b 的值 : %d\n" , b ) }func swap (x, y int ) int { var temp int temp = x x = y y = temp return temp; }
运行的结果为:
1 2 3 4 交换前 a 的值为 : 100 交换前 b 的值为 : 200 交换后 a 的值 : 100 交换后 b 的值 : 200
GO指针
和C++以及C中的是一样的,对go中的指针定义的时候 *int
传递变量的地址&
在对一个指针赋值的时候,传递的是某一个变量的地址,就是传递这个变量的引用,引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
defer
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数 ,主要作用:
如果一个函数中有多个defer语句,它们会以LIFO(后进先出) 的顺序执行。
1 2 3 4 5 6 7 8 9 func Demo () { defer fmt.Println("1" ) defer fmt.Println("2" ) defer fmt.Println("3" ) defer fmt.Println("4" ) }func main () { Demo() }
输出的内容
数组与切片
Go 语言切片 slice
是对数组的抽象
静态数组
通过这种方式进行初始化数组以及进行切片操作,通过range关键字进行遍历数组,并给出index和value进行给出不同的下标和数值
固定数组传递的是一个值拷贝
动态数组 slice
切片不需要说明长度
1 2 3 4 5 6 7 8 9 10 11 12 slice1 := []int {1 , 2 , 3 } s := arr[:] slice1 = make ([]int , 4 )var slice2 []int
也可以指定容量,其中capacity
为可选参数。
1 make ([]T, length, capacity)
将arr中从下标startIndex到endIndex-1
下的元素创建为一个新的切片
1 s := arr[startIndex:endIndex]
缺省endIndex时将表示一直到arr的最后一个元素,缺省startIndex时将表示从arr的第一个元素开始
1 2 s := arr[startIndex:] s := arr[:endIndex]
通过切片s初始化切片s1
1 s1 := s[startIndex:endIndex]
通过内置函数 make()
初始化切片s,[]int
标识为其元素类型为int的切片
同时动态数组传递的过程中的参数形式是一致的,能够适配所有的slice参数类型,但是对于
这里面的下划线表示的是不需要考虑 的index的数值,可以忽略,这里是关于切片slice的声明和打印
1 2 3 4 5 slice1 :=[]int {1 ,2 ,3 } fmt.Printf("len = %d, slice =%v\n" , len (slice1),slice1) }
打印的结果是
声明slice但是不一定声明了空间,因此需要注意的是声明的同时并给出空间大小,同时没办法中途增加空间
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 func main () { slice1 :=[]int {1 ,2 ,3 } fmt.Printf("len = %d, slice =%v\n" , len (slice1),slice1) slice1 = make ([]int ,4 ) fmt.Printf("len = %d, slice =%v\n" , len (slice1),slice1) var slice2 []int fmt.Printf("len = %d, slice =%v\n" , len (slice2),slice2) slice2 = make ([]int ,3 ) fmt.Printf("len = %d, slice =%v\n" , len (slice2),slice2) slice2[0 ]=1000 fmt.Printf("len = %d, slice =%v\n" , len (slice2),slice2) var slice3 =make ([]int ,5 ) fmt.Printf("len = %d, slice =%v\n" , len (slice3),slice3)
判断一个切片是不是空的
1 2 3 4 5 6 7 8 9 if slice1 == nil { fmt.Println("slice1 is null" ) }else { fmt.Println("slice1 is not null" ) } }
注意if-else的格式有要求,{
必须是出现在else和if紧接着的位置,不能换行写
append()和copy()
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来
注意,如果append超过了当前的空间,那么slice就会继续增加空间,增加的大小是cap的大小增加
拷贝copy()操作
1 2 3 4 5 6 7 numbers1 := make ([]int , len (numbers), (cap (numbers))*2 ) copy (numbers1,numbers) printSlice(numbers1)
关于切片的截取操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var slice6 = make ([]int ,3 ) slice6[0 ]=1 slice6[2 ]=33 fmt.Printf("len = %d, cap = %d, slice =%v\n" , len (slice6), cap (slice6), slice6) s1:=slice6[0 :2 ] fmt.Println(s1) var s2 = make ([]int ,3 ) copy (s2,slice6) fmt.Println(s2)
map
map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。
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 49 50 51 52 53 54 55 56 57 package mainimport ( "fmt" )func main () { var test1 map [string ]string test1 = make (map [string ]string , 10 ) test1["one" ] = "php" test1["two" ] = "golang" test1["three" ] = "java" fmt.Println(test1) test2 := make (map [string ]string ) test2["one" ] = "php" test2["two" ] = "golang" test2["three" ] = "java" fmt.Println(test2) test3 := map [string ]string { "one" : "php" , "two" : "golang" , "three" : "java" , } fmt.Println(test3) language := make (map [string ]map [string ]string ) language["php" ] = make (map [string ]string , 2 ) language["php" ]["id" ] = "1" language["php" ]["desc" ] = "php是世界上最美的语言" language["golang" ] = make (map [string ]string , 2 ) language["golang" ]["id" ] = "2" language["golang" ]["desc" ] = "golang抗并发非常good" fmt.Println(language) fmt.Println(language) }
面向对象结构体
定义一个结构体
1 2 3 4 type T struct { name string }
分别定义不同的拷贝和引用的函数
1 2 3 4 5 6 7 func (t T) method1() { t.name = "new name1" }func (t *T) method2() { t.name = "new name2" }
结果是使用值拷贝的输出的name没有改变,只有使用引用的才发生了改变
关于结构体定义的细节,内部的成员变量和结构体本身的大小写就是蕴含了是不是私有和公有的关系,大写标识公有,小写表示私有
1 2 3 4 5 6 type Human struct { Name string Age int }
对于结构体内部的成员函数,必须是传递了引用的地址才能够修改,否则就是默认的值传递
1 2 3 4 5 6 7 8 9 10 11 func (this *Human) GetName() { fmt.Println(this.Name) }func (this *Human) SetName(newname string ) { this.Name = newname }func (this Human) SetName1(newname string ) { this.Name = newname }
类的继承性
如果新定义的类继承了某个类,那么只需要在内部写上所继承的类的名称,同时这里没有C++中的公有保护等其他类型的继承,公有私有的设定保持一致
1 2 3 4 5 type Superman struct { Human Level int }
对继承类中的方法重写,同样传递的还是引用和指针
1 2 3 4 5 6 func (this *Superman) GetName() { fmt.Println(this.Name) fmt.Println(this.Level) }
重新定义新的方法
1 2 3 4 5 6 func (this *Superman) LevelUp() { fmt.Println("level up" ) this.Level = this.Level + 1 fmt.Println(this) }
关于主函数中的调用
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 func main () { human := Human{Name: "zhang" , Age: 99 } human.SetName1("li" ) fmt.Println(human) human.SetName("li" ) fmt.Println(human) human.GetName() fmt.Println("-------------" ) superman := Superman{Human{"li4" , 18 }, 99 } superman.GetName() superman.LevelUp() superman.SetName("wang5" ) fmt.Println(superman) fmt.Println("+++++++++++++" ) var super Superman super.Name = "zhangmazi" super.Level = 100 super.Age = 19 fmt.Println(super) }
Interface与类型断言
在继承和多态上,一系列家族定义的接口,每个子类能够重写方法 ,实现同一个方法有多个接口表现形式
类的多态性
本质上利用interface来实现类的多态性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Animal interface { Sleep() GetColor() string GetType() string }type Cat struct { color string }type Dog struct { color string }
那怎么认为这个cat继承了这个animal类呢?只需要对animal中的所有函数重写即可认为是继承了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (this *Cat) Sleep() { fmt.Println("cat sleep..." ) }func (this *Cat) GetColor() string { fmt.Printf("the cat color is %v\n" , this.color) return this.color }func (this *Cat) GetType() string { fmt.Printf("the type is cat\n" ) return "Cat" }
同理对于dog也是一样
1 2 3 4 5 6 7 8 9 10 11 12 func (this *Dog) Sleep() { fmt.Println("dog sleep..." ) }func (this *Dog) GetColor() string { fmt.Printf("the dog color is %v\n" , this.color) return this.color }func (this *Dog) GetType() string { fmt.Printf("the type is dog\n" ) return "Dog" }
主函数中如何实现不同的多态调用呢?注意哦,这个地方传递的是继承类的引用进去来实现多态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func showanimal (animal Animal) { animal.Sleep() animal.GetType() animal.GetColor() } cat := Cat{"Green" } dog := Dog{"Yellow" } showanimal(&cat) showanimal(&dog)
万能类型interface
golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}
的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})
的方式传进来的时候,也就意味着这个参数被自动的转为interface{}
的类型。
1 2 3 func funcName (a interface {}) string { return string (a) }
interface{}
相当于是一个万能的数据类型,适用于对任何的函数的参数传递中的使用
1 2 var a interface {} fmt.Println("Where are you,Jonny?" , a.(string ))
如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断
如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true ,同时value将会得到所期待的正确的值。
interface{}的例子
定义一个断言类型的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func myFunc (arg interface {}) { fmt.Println("myfunc is called..." ) fmt.Println(arg) value, ok := arg.(string ) if !ok { fmt.Println("arg is not a string" ) fmt.Printf("the value is %T\n" , arg) } else { fmt.Println("arg is string type,is = " , value) } }
主函数的调用关系如下:
1 2 3 4 5 6 7 8 9 type Book struct { auth string }func main () { book := Book{"golang" } myFunc(book) myFunc(100 ) myFunc("goland" ) }
输出的内容是:
1 2 3 4 5 6 7 8 9 10 11 myfunc is called... {golang} arg is not a string the value is main.Book myfunc is called... 100 arg is not a string the value is int myfunc is called... goland
Reflect反射
在讲反射之前,先来看看Golang关于类型设计的一些原则
变量包括(type, value)两部分
type 包括 static type
和concrete type
.
简单来说
static type
是你在编码是看见的类型(如int、string),concrete type
是runtime
系统看见的类型
类型断言能否成功,取决于变量的concrete type
,而不是static type
.
因此,一个
reader
变量如果它的concrete type
也实现了write
方法的话,它也可以被类型断言为writer
.
反射
,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static
type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关 (它的type是concrete
type),只有interface类型才有反射一说
在Golang的实现中,每个interface
变量都有一个对应pair
,pair中记录了实际变量的值和类型
value是实际变量值,type是实际变量的类型。一个interface{}
类型的变量包含了2个指针,一个指针指向值的类型concrete type
,另外一个指针指向实际的值对应value
reflect的基本功能
reflect的反射类型对象:TypeOf和ValueOf
那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢?
它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf()
和 reflect.TypeOf()
1 2 3 4 5 6 7 8 9 10 11 12 func ValueOf (i interface {}) Value {...}func TypeOf (i interface {}) Type {...}
reflect.TypeOf()
是获取pair中的type,reflect.ValueOf()
获取pair中的value,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "reflect" )func main () { var num float64 = 1.2345 fmt.Println("type: " , reflect.TypeOf(num)) fmt.Println("value: " , reflect.ValueOf(num)) } 运行结果:type : float64 value: 1.2345
说明:
reflect.TypeOf:
直接给到了我们想要的type类型,如float64、int、各种pointer、struct
等等真实的类型
reflect.ValueOf: 直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1
"Allen.Wu" 25} 这样的结构体struct的值
反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type
和reflect.Value这
两种
reflet例子1:
1 2 3 4 func reflectNum (arg interface {}) { fmt.Println("type:" , reflect.TypeOf(arg)) fmt.Println("value:" , reflect.ValueOf(arg)) }
主函数的调用
1 2 3 4 func main () { var num float64 = 1.2345 reflectNum(num) }
注意,在使用反射之前需要引入reflect的包
1 2 3 4 import ( "fmt" "reflect" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { var num float64 = 1.2345 pointer := reflect.ValueOf(&num) value := reflect.ValueOf(num) convertPointer := pointer.Interface().(*float64 ) convertValue := value.Interface().(float64 ) fmt.Println(convertPointer) fmt.Println(convertValue) } 运行结果:0xc42000e238 1.2345
reflet例子2:
首先定义一个类以及关于这个类的方法
1 2 3 4 5 6 7 8 9 10 type User struct { Id int Name string Age int }func (this User) Call() { fmt.Println("user is called .." ) fmt.Printf("%v\n" , this) }
再定义一个利用反射选择类中值和方法的函数
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 func DoFiledAndMethod (input interface {}) { inputType := reflect.TypeOf(input) fmt.Println("input type is: " , inputType.Name()) inputValue := reflect.ValueOf(input) fmt.Println("input value is: " , inputValue) for i := 0 ; i < inputType.NumField(); i++ { field := inputType.Field(i) value := inputValue.Field(i).Interface() fmt.Printf("%s: %v = %v\n" , field.Name, field.Type, value) } for i := 0 ; i < inputType.NumMethod(); i++ { m := inputType.Method(i) fmt.Printf("%s: %v\n" , m.Name, m.Type) } }
注意点:
reflect.TypeOf (input)得到类型
reflect.ValueOf (input)得到对应的值
reflect.TypeOf (input).NumField() 的方法是获得interface()中的所有的字段
如果选择字段中的类型: inputType.Field(i) =
reflect.TypeOf(input).Field(i)
如果选择字段中的值:inputType.Field(i) =
reflect.TypeOf(input).Field(i).Interface()
如果想便利interface中的方法:reflect.TypeOf (input).NumMethod() ,其中具体的方法是:reflect.TypeOf(input).Method(i)
通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:
先获取interface的reflect.Type,然后通过NumMethod进行遍历
再分别通过reflect.Type的Method获取对应的真实的方法(函数)
最后对结果取其Name和Type得知具体的方法名
也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.Value设置实际变量的值
reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。
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 mainimport ( "fmt" "reflect" )func main () { var num float64 = 1.2345 fmt.Println("old value of pointer:" , num) pointer := reflect.ValueOf(&num) newValue := pointer.Elem() fmt.Println("type of pointer:" , newValue.Type()) fmt.Println("settability of pointer:" , newValue.CanSet()) newValue.SetFloat(77 ) fmt.Println("new value of pointer:" , num) pointer = reflect.ValueOf(num) } 运行结果: old value of pointer: 1.2345 type of pointer: float64 settability of pointer: true new value of pointer: 77
需要传入的参数是*
float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针 。
如果传入的参数不是指针,而是变量,那么
通过Elem获取原始值对应的对象则直接panic
通过CanSet方法查询是否可以设置返回false
newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
reflect.Value.Elem()
表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的,是指只能修改原是对象的值的大小,不能修改地址
newValue.SetFloat(77)重新设置值的操作,传递引用来修改interface中的值
反射的基本原理
反射基本原理总结
结构体标签
本质上还是利用了反射,通过以下形式给结构体中的变量添加标签作用:
其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用。
作用:能够将结构体转化为json格式
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 package mainimport ( "fmt" "reflect" )type resume struct { Name string `info:"name" doc:"我的名字"` Sex string `info:"sex"` }func findtag (str interface {}) { t :=reflect.TypeOf(str).Elem() for i:=0 ;i<t.NumField();i++{ tagstring:=t.Field(i).Tag.Get("info" ) tagdoc:=t.Field(i).Tag.Get("doc" ) fmt.Println("info:" ,tagstring,"doc" ,tagdoc) } }func main () { var re resume findtag(&re) }
输出之后在json格式转换中可以看到如下,注意可以看到的是输出的内容是根据给定的tag来进行标题的命名的
利用反射取出元素查询
利用编码和解码对struct 和json之间的转化
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 package mainimport ( "fmt" "encoding/json" )type Movie struct { Title string `json:"title"` Year int `json:"year"` Price int `json:"rmb"` Actors []string `json:"actors"` }func main () { movie :=Movie{"喜剧之王" ,2000 ,10 ,[]string {"zhouxingchi" ,"zhangbozhi" }} jsonStr,err:=json.Marshal(movie) if err!=nil { fmt.Println("json marshal error" ,err) return } fmt.Printf("jsonStr=%s\n" ,jsonStr ) myMovie:=Movie{} err=json.Unmarshal(jsonStr,&myMovie) if err!=nil { fmt.Println("json unmashal error" ,err) return } fmt.Printf("%v\n" ,myMovie) }