Go基础

Go语言是静态类型语言,所有的内存在 Go 中都是经过初始化的,当一个变量被声明之后,系统自动赋予它该类型的零值int0float0.0boolfalsestring空字符串指针nil 等。

只有两个相同类型的值才可以进行比较,如果值的类型是接口interface),那么它们也必须都实现了相同的接口&&的优先级比||高(&& 对应逻辑乘法,|| 对应逻辑加法,乘法比加法优先级要高)。

变量函数常量名称如果首字母大写,则表示它可被其它的包访问;如果首字母小写,则表示它只能在本包中使用。

nil 不是关键字或保留字且不能比较。

数据类型

Go语言的基本类型有:

  • bool
  • string
  • intint8int16int32int64
  • uintuint8uint16uint32uint64uintptr(只有在底层编程时才需要)
  • byteuint8 的别名)
  • runeint32 的别名 代表一个 Unicode 码点)
  • float32float64
  • complex64complex128

尽管在某些特定的运行环境下 intuintuintptr 的大小可能相等,但是它们依然是不同的类型,在需要把 int 类型当做 int32 类型使用的时候必须显示的对类型进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const num1 int = 1
var num1, num2, num3 = 1, 2, 3
var num1, num2, num3 = 1, "2", 3.5
var (
num1 int
num2 string
num3 float64
)

var (
num1 = 1
num2 = "2"
num3 = 3.5
)

Go是一门静态类型语言,每个变量都有一个在编译时就确定的静态类型。虽然ab的的基本类型相同,但静态类型不同,无类型转换的情况下无法相互赋值。虽然在运行时中,接口变量存储的值也许会变,但接口变量的类型是不会变的。

1
2
3
4
type MyInt int

var a int
var b MyInt

容器

Go语言常用容器有数组切片MapList。map和切片是不可以用==直接被比较的。

数组的长度必须是常量表达式,若数组长度的位置出现...省略号,则表示数组的长度是根据初始化值的个数来计算。数组的长度是数组类型的一个组成部分,若两个数组类型相同(包括数组的长度,数组中元素的类型),可直接通过较运算符==!=来判断两个数组是否相等,不能比较两个类型不同的数组,且不能相互赋值,否则程序将无法完成编译。

1
2
3
4
var numArr = [3]int{2, 3, 4,}
strArr := [3]int{"2", "3", "4"}

array := [5]int{1:10, 3:30}

切片slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,终止索引标识的项不包括在切片内。切片的内部结构包含地址大小容量,切片一般用于快速地操作一块数据集合。切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充。在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次。make函数创建切片时若只指定长度,则切片的长度和容量相等,不允许创建长度大于容量的切片

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
var numbers = [4]int{1, 2}
var arr = [...]int{1, 2, 3, 4, 5,}
slice := arr[:]
slice := arr[1:3]
slice := arr[1:]
slice := arr[:3]

var slice []int
slice = append(slice, 1)

var slice = make([]int, 2, 5)
slice = append(slice, 1, 2, 3, 4)
slice = append(slice, []int{5, 6, 7}...)
slice = append([]int{-3, -2, -1}, slice...)

slice := []int{1, 2, 3, 4}
slice = append(slice[:2], append([]int{6, 7}, slice[2:]...)...)

sliceA := []int{1, 2, 3, 4, 5}
sliceB := []int{6, 7, 8}
copyCount := copy(sliceA, sliceB)

sliceB := []int{1, 2, 3, 4, 5}
sliceA := []int{6, 7, 8}
copyCount := copy(sliceA, sliceB)
// 创建容量和长度都是100的切片
slice := []int{99:0}

计算切片的长度容量,若底层数组容量k的切片slice[i:j],长度为j-i,容量为k-i。也可以通过第三个索引来控制新切片的容量。若底层数组容量k的切片slice[i:j:s],长度为j-i,容量为s-is<=K

map 是引用类型,可动态增长,未初始化的 map 的值是 nil,使用函数 len() 可以获取 mappair 的数目。map不能使用cap()函数。定义map时可现实指定容量,当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1。map可以存函数。map无序的。切片函数以及包含切片的结构类型由于具有引用语意不能作为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
var mapLit = map[int]string{}
var mapLit map[int]string

var mapLit = make(map[int]string, 16)

mapLit := map[int]string{1: "a", 2: "b"}
mapLit[3] = "c"
for key, value := range mapLit {
fmt.Println("key value:", key, value)
}
delete(mapLit, 1)

mapLit := map[string]int{
"hello": 100,
"world": 200,
}

if value, isExist := mapLit["hello"];isExist:
fmt.Println("value:", value)

skill := map[string]func(){
"fire": func() {
fmt.Println("chicken fire")
},
}
f, ok := skill["fire"]

列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系。

1
2
3
4
5
6
7
8
9
lit := list.New()
lit.PushBack("AA")
lit.PushFront("BB")
element := lit.PushFront("CC")
lit.InsertBefore("DD", element)
for i := lit.Front(); i != nil; i = i.Next() {
fmt.Println("lit value:", i.Value)
}
lit.Remove(element)

Go语言线程安全的sync.MapRange返回为false时将不再往下遍历。

1
2
3
4
5
6
7
8
var syncMap sync.Map
syncMap.Store(1, "a")
syncMap.Store(2, "b")
value, ok := syncMap.Load(2)
syncMap.Range(func(key, value interface{}) bool {
fmt.Println("key, value", key, value)
return true
})

流程控制

Go 语言常用流程控制有 iffor,而 switchgoto 主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。

if-else分支结构,可结合goto使用。

1
2
3
4
5
6
7
8
9
if index := 12; index > 10 {
} else {
}

if index := 10; index == 10 {
goto onExit
}
onExit:
fmt.Println("exit")

go中只有for循环结构,不支持 whiledo-while 结构;for range 可以遍历数组切片字符串map通道channel);其中用到的range 返回的是每个元素的副本,而不是直接返回对该元素的引用。字符串的遍历是一个个rune 字符。

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
JLoop:
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
break JLoop
}
}
}

var index int
for index < 10 {
index++
}

str := "12456789"
for pos, char := range str {
fmt.Println("pos, char:", pos, char)
}

channel := make(chan int)
go func() {
channel <- 1
channel <- 2
channel <- 3
close(channel)
}()
for value := range channel {
fmt.Println("channel value:", value)
}

switch表达式不需要为常量,甚至不需要为整数case 按照从上到下的顺序进行求值,直到找到匹配的项,若switch 没有表达式,则对 true 进行匹配。fallthrough会紧接着执行下一个 case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
str := "kk"
switch str {
case "hello", "kk":
fmt.Println(1)
fallthrough
case "world":
fmt.Println(2)
default:
fmt.Println(1)
}

r := 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}

函数

Go 语言支持普通函数匿名函数闭包。函数间传递变量总是以值得方式传递,数组传递会完整复制并传递给函数,最好只传入指向数组的指针。函数间传递切片和map只会复制切片和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
func funcA() (a, b int) {
a = 1
b = 2
return
}
a, b := funcA()

func funcB() (int, int) {
return 3, 4
}
a, b := funcB()
f := funcB
a, b := f()

func funcC() (a, b string, c int) {
return "a1", "b2", 5
}
a, b, c := funcC()

func funcD(arr []int, f func(int)) {
for _, value := range arr {
f(value)
}
}
funcD([]int{1, 2, 3, 4}, func(data int) {
fmt.Println("this value:", data)
})

// 函数变量
var f func() (int, int)
f = funcA
a, b := f()

// 匿名函数
func(data int) {
fmt.Println("inner func:", data)
}(100)

f := func(data int) {
fmt.Println("inner func:", data)
}
f(500)

闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除。被捕获到闭包中的变量让闭包本身拥有了记忆效应。

1
2
3
4
5
6
7
8
9
10
func accumulate(value int) func() int {
return func() int {
value++
return value
}
}

accumulator := accumulate(1)
fmt.Println(accumulator()) // 2
fmt.Println(accumulator()) // 3

可变参数和任意类型的可以变参数,用 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
func notFixedParam(args ...int) {
}

func notFixedParamV2(format string, args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
case string:
case int64:
default:
}
}
}

notFixedParam(1, 2)
notFixedParamV2("kk", 1, 234, "hello", 3.14)

func rawPrint(rawList ...interface{}) {
for _, raw := range rawList {
fmt.Println(raw)
}
}

func print(slist ...interface{}) {
rawPrint(slist...)
}

defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句defer 的逆序进行执行。类似javafinally语句块。可与宕机panic 一起使用,宕机前会优先执行defer。提供recover 用于宕机恢复,且仅在延迟函数 defer 中有效。正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。

1
2
defer fmt.Println("宕机后要做的事情")
panic("宕机")

结构体

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。使用new&构造的类型实例的类型是类型的指针Go语言的类型结构体没有构造函数的功能.

使用.来访问结构体的成员变量,访问结构体指针的成员变量时可以继续使用.,Go使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name;对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作;

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
type Color struct {
R, G, B byte
}

color := new(Color)
(*color).R = 12
color.G = 16

color := &Color{}
(*color).R = 12
color.G = 16

type Command struct {
Name string
Var *int
Comment string
}


version := 1
// 使用键值对填充结构体
cmd := &Command{
Name: "version",
Var: &version,
Comment: "show version",
}
fmt.Println("cmd, Name, Var, Comment:", *cmd, cmd.Name, *cmd.Var, cmd.Comment)

// 使用多个值的列表初始化结构体
cmd := Command{
"version",
&version,
"show version",
}
fmt.Println("cmd, Name, Var, Comment:", cmd, cmd.Name, *cmd.Var, cmd.Comment)

匿名结构体没有类型名称,无须通过 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
ins := struct {
Name string
Var *int
Comment string
}{
"version",
&version,
"show version",
}

func printMsg (msg *struct{
id int
data string
}){
fmt.Println("msg", msg)
}

msg := &struct {
id int
data string
}{
1024,
"hello",
}
printMsg(msg)

结构体可以包含一个或多个匿名(或内嵌)字段,没有显式的名字,只有字段的类型,此时类型也就是字段的名。在一个结构体中对于每一种数据类型只能有一个匿名字段。结构体可以包含内嵌结构体,内嵌结构体甚至可以来自其他包。

结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type innerS struct {
in1 int
in2 int
}

type outerS struct {
b int
c float32
in1 int
int
innerS
}

outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 20
outer.innerS.in1 = 5
outer.in2 = 10
fmt.Println("outer1 :", *outer)
outer2 := outerS{6, 7.5, 20, 60, innerS{5, 10}}
fmt.Println("outer2 :", outer2)

结构体标签是对结构体字段的额外信息标签,由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。标签内容是静态的,无须实例化结构体,可以通过反射从结构体中获取标签内容。

1
2
3
4
5
6
7
8
9
type Ins struct {
in1 int `key1:"value1" key2:"value2"`
in2 int `key1:"value1" key2:"value2"`
}

typeOfIns := reflect.TypeOf(Ins{})
if catType, ok := typeOfIns.FieldByName("in2"); ok {
fmt.Println(catType.Tag.Get("key1"))
}

方法能给用户定义的类型添加新的行为,实际上也是函数,仅仅是在申明时,在关键字func和方法名间增加了一个参数,该参数称为接收者值接收者指针接收者),有接收者为方法,无接收者为函数。值接收者调用时会使用这个值的副本来执行,如下所示changeEmailV2方法是无效的。

1
2
3
4
5
6
7
8
9
10
11
12
type cusUser struct {
name string
email string
}

func (u *cusUser) changeEmailV1(email string) {
u.email = email
}

func (u cusUser) changeEmailV2(email string) {
u.email = email
}

若要创建一个新值,该类型的方法使用值接收者,若要修改当前值,使用指针接收者。

接口

Go无类和继承的概念,Go语言的接口在命名时,一般会在单词后面添加 er;当方法名首字母大写时,且该接口类型名首字母也大写时,该方法可被接口所在的包之外的代码访问。

接口被实现必须满足,接口的方法与实现接口的类型方法格式一致,接口中所有方法均被现实。类型和接口之间有一对多和多对一的关系。一个类型可以同时实现多个接口,一个接口的方法,不一定需要由一个类型完全实现。

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。

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
type DataWriter interface {
WriteData(data interface{}) error

CanWriter() bool
}

type Closer interface {
WriterClose() error
}

type FileWriter struct {
}

type NetWriter struct {
FileWriter
}

func(d *FileWriter) WriteData(data interface{}) error {
fmt.Println("WriteData:", data)
return nil
}

func(d *FileWriter) WriterClose() error {
fmt.Println("FileWriter Close")
return nil
}

func(d *NetWriter) CanWriter() bool {
fmt.Println("CanWrite")
return false
}

fileWriter := new(FileWriter)
fileWriter.WriterClose()
fileWriter.WriteData("FileWriter")

netWriter := new(NetWriter)
netWriter.WriterClose()
netWriter.WriteData("NetWriter")
netWriter.CanWriter()

f := new(NetWriter)
var writer DataWriter
writer = f
writer.WriteData("NetWriter2")
writer.CanWriter()

var writer DataWriter = new(NetWriter)
writer = f
writer.WriteData("NetWriter3")
writer.CanWriter()

var writeCloser DataWriteCloser = new(NetWriter)
writeCloser.WriterClose()
writeCloser.WriteData("writeCloser")
writeCloser.CanWriter()

将接口转换为其他接口,若不写ok接收是否为实现该类型,若rw没有完全实现接口,将触发宕机。

1
2
3
var writeCloser DataWriteCloser = new(NetWriter)
rw, ok := writeCloser.(Closer)
fmt.Println(ok, ";", rw.WriterClose())

将接口转换为其他类型时,接口内保存的实例对应的类型指针,必须是要转换的对应的类型指针。

1
2
3
var writeCloser DataWriteCloser = new(NetWriter)
rw, ok := writeCloser.(*NetWriter)
fmt.Println(ok, ";", rw.WriterClose(), rw.WriteData("*NetWriter"), rw.CanWriter())

接口在底层的实现有typedata两个部分。显式地将 nil 赋值给接口时,接口的 typedata 都将为 nil,此时接口与 nil 值判断是相等的。将带有类型的 nil 赋值给接口时,只有 datanil,而type 不为 nil,此时接口与 nil 判断将不相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type insImpl struct {
}

func (ins *insImpl) String() string {
return "hi"
}

func GetStringer() fmt.Stringer{
var ins *insImpl = nil
return ins
}
// false
fmt.Println(GetStringer() == nil)
// true
var ins *insImpl = nil
fmt.Println(ins == nil)

Go的源码复用建立在package的基础上,入口 main() 函数所在的包(package)叫 main。包与文件夹一一对应,一般包的名称就是其源文件所在目录的名称,所有与包相关的操作,必须依赖于工作目录GOPATH。包可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用

包的习惯用法:

  • 包名一般小写,使用简短且有意义的名称。
  • 包名一般和所在目录同名,也可不同,包名中不能包含-等特殊符号。
  • 包一般使用域名作为目录名称,能保证包名的唯一性。
  • 包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
  • 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。

包的引用路径分为全路径导入相对路径导入。可以自定义别名引用包;也可用.省略引用格式,相当于把 包直接合并到当前程序中,在使用 包内的方法是可以不用加前缀直接引用;若只执行包初始化的 init 函数,不使用包内部的数据可使用匿名引用格式;

1
2
3
4
5
6
7
8
9
10
// 全路径导入:源码位于GOPATH/src/lab/test目录下
import "lab/test"
// 相对路径导入:源码位GOPATH/src/lab/a目录下
import "../a"
// 自定义别名引用包
import F "fmt"
// 省略引用格式
import . "fmt"
// 匿名引用格式
import _ "fmt"

一个包可有多个 init 函数,包加载时会执行全部 init 函数,但不能保证执行顺序;包不能出现环形引用;包允许重复引用;包初始化程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图;编译器会将有向无环图转换为一棵树,然后从树的叶子节点开始逐层向上对包进行初始化;单个包的初始化先初始化常量,然后是全局变量,最后执行包的 init 函数

并发

Go 的并发通过 goroutine特性完成。goroutine 类似于线程,但是可根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 运行时调度完成,而线程是由操作系统调度完成

Go 提供 channel在多个 goroutine 间进行通信,channel类型相关语言级别goroutine间的进程内的通信方式。必须使用 make 创建 channel;可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

1
2
3
4
5
6
7
8
9
10
11
12
ch := make(chan int)
go func() { ch <- 1 }()

ch := make(chan interface{})
go func() { ch <- "hi" }()

type Equip struct {
a int
b int
}
ch := make(chan *Equip)
go func() { ch <- &Equip{a: 1, b: 2} }()

通道使用<-操作符发送接收数据;把数据往通道中发送时,若接收方一直未接收,发送操作将持续阻塞;通道的收发操作在不同的两个 goroutine 间进行;接收将持续阻塞直到发送方发送数据;每次接收一个元素;被关闭的通道不会被置为 nil。对已经关闭的通道进行发送,将会触发宕机。从已关闭的通道接收数据或者正在接收数据时,将会接收到通道类型的零值,然后停止阻塞并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 阻塞接收数据
data := <-ch
// 非阻塞接收数据,可能造成高CPU占用,很少使用,若ok未false表示通道ch已关闭
data, ok := <-ch
// 接收任意数据,忽略接收数据
<- ch
// 声明一个只能发送的通道类型
var chSendOnly = make(chan<- int)
// 声明一个只能接收的通道类型
var chRecvOnly = make(<-chan int)
// 关闭通道
close(chSendOnly)
// 带缓冲的通道,缓冲通道被填满时,发送数据时发生阻塞,带缓冲通道为空时,接收数据时发生阻塞
ch := make(chan int, 3)

协程有独立的栈空间共享堆空间,调度由用户自己控制,本质上类似于用户级线程,这些用户级线程的调度也是自己实现的。一个线程上可以跑多个协程,协程是轻量级的线程

使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。若要在 goroutine 中返回数据,通过通道把数据从 goroutine 中作为返回值传出。

1
2
3
go func(param1, param2 int) {
fmt.Println("param3, param4:", param1, param2)
}(3, 4)

Go未为channel专门设置超时处理机制,但可通过select来设置超时。select用法与switch很类似,但select中只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case情况,且每个case语句里必须是一个IO操作,select是按顺序从头至尾评估。若无语句可执行,则执行default语句,否则被阻塞。

1
2
3
4
5
6
7
8
9
10
ch := make(chan int)
quit := make(chan bool)

select {
case num := <-ch:
fmt.Println("num:", num)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
quit <- true
}

Gosync包中提供互斥锁sync.Mutex和读写互斥锁sync.RWMutex。同时提供了等待组sync.WaitGroup进行多个任务的同步,每个 sync.WaitGroup 值在内部维护着一个计数,保证在并发环境中完成指定数量的任务,若WaitGroup的值大于0,Wait方法就会被阻塞。同时提供原子访问atomic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var rw sync.Mutex
rw.Lock()
defer rw.Unlock()

var rw sync.RWMutex
rw.RLock()
defer rw.RUnlock()
rw.Lock()
defer rw.Unlock()

var wg sync.WaitGroup
wg.Add(1)
go func(){
defer wg.Done()
}()
wg.Wait()

atomic.AddInt32(&counter, 1)
// 安全的写整型值
atomic.StoreInt64(&counter, 1)
// 安全的读整型值
atomic.LoadInt64(&counter)

死锁发生条件:

  • 互斥条件
  • 请求和保持条件
  • 不剥夺条件
  • 环路等待

解决办法:

  • 并发范文多个表,约定访问顺序
  • 同一事物中,尽可能一次锁定获取所需资源
  • 容易死锁业务场景,尝试升级锁颗粒度
  • 采用分布式事务锁或乐观锁

反射

reflect 包定义了两个重要的类型 TypeValue 任意接口值在反射中都可以理解为由 reflect.Typereflect.Value 两部分组成;提供了reflect.TypeOfreflect.ValueOf 两个函数来获取任意对象的 ValueType

反射中分类型Type种类Kind,当需要区分一个大品种的类型时用KindKind方法描述的是基础类型;如需统一判断类型中的指针时。而Type是指系统原生数据类型,以及使用type关键字定义的类型。通过 reflect.Elem() 方法获取该指针指向的元素类型Elem方法能够对指针进行解引用,然后将结果存储到反射 Value 类型对象 中。

反射可以将接口类型变量转换为反射类型对象,反射可以将反射类型对象转换为接口类型变量,若要修改反射类型对象值必须是可写的。可通过 CanSet 方法检查reflect.Value类型变量的可写性。对于不具有可写性Value 类型变量,调用 Set 方法会报错

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
type T struct {
A int
B string
}
t := &T{}
typeOfT := reflect.TypeOf(t)
// Name: ,Kind:ptr
fmt.Println("name:", typeOfT.Name(), "kind:", typeOfT.Kind())
// 通过 reflect.Elem() 方法获取这个指针指向的元素类型
typeOfT = typeOfT.Elem()
// Name: T,Kind: struct
fmt.Println("name:", typeOfT.Name(), "kind:", typeOfT.Kind())
typeOfT.Field(0).SetInt(99)
typeOfT.Field(1).SetString("Sunset Strip")

t := T{23, "skidoo"}
typeOfA := reflect.TypeOf(t)
// Name: T,Kind: struct
fmt.Println("name:", typeOfA.Name(), "kind:", typeOfA.Kind())

type MyInt int
var x MyInt = 7
tof := reflect.TypeOf(x)
// Name: MyInt,Kind: int
fmt.Println("name:", tof.Name(), "kind:", tof.Kind())
vof := reflect.ValueOf(x)
// 不能是int
ref := vof.Interface().(MyInt)
fmt.Println("can set:", vof.CanSet()) // false
vof2 := reflect.ValueOf(&x)
fmt.Println("can set:", vof2.CanSet()) // false
vof3 := vof2.Elem()
fmt.Println("can set:", vof3.CanSet()) // true
vof3.SetInt(25)
fmt.Println("ref:", vof3) // 25

MapSliceChan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类Kind,不属于 Ptr。所有通过reflect.ValueOf(x) 返回的reflect.Value不可以取地址,通过指针间接地获取的reflect.Value都可以取地址,即使开始的是一个不可取地址的Value。当reflect.Value不可寻址时,使用 Addr() 方法也无法取到值的地址

已知reflect.Type时,可动态地创建这个类型的实例,实例的类型为指针

1
2
3
4
5
var a int
typeOfA := reflect.TypeOf(a)
aIns := reflect.New(typeOfA)
// name: *int, kind: ptr
fmt.Println("name:", aIns.Type(), "kind:", aIns.Kind())

使用反射调用函数时,需将参数使用反射值对象的切片[]reflect.Value构造后传入Call()方法中,调用完成时,函数的返回值通过[]reflect.Value返回。

1
2
3
4
5
6
funcVal := reflect.ValueOf(func(a, b int) int {
return a + b
})
paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(24)}
retList := funcVal.Call(paramList)
fmt.Println("result:", retList[0].Int())

Test

  • 测试用例文件不会参与正常源码的编译不会被包含到可执行文件中
  • 测试用例的文件名必须以_test.go结尾;
  • 需要使用 import 导入 testing 包;
  • 测试函数的名称要以TestBenchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;
  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。

SetFinalizer

Go语言自带垃圾回收机制,GC 是自动进行的,可以使用 runtime.GC() 函数手动 GC

finalizer终止器)是通过 runtime.SetFinalizer 来设置与对象关联的一个函数,若某对象定义了 finalizer,当它被 GC 时候,该finalizer 将被调用。

1
func SetFinalizer(x, f interface{})

参数x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针,参数 f 必须是一个函数SetFinalizer 函数将 x 的终止器设置为 f,当垃圾收集器发现 x 不能再直接或间接访问时,则清理 x 并调用 f(x),终止器会在 x 不能直接或间接访问后的任意时间被调用执行,不保证终止器会在程序退出前执行,因此一般终止器只用于在长期运行的程序中释放关联到某对象的非内存资源。*x 的大小为 0 字节,也不保证终止器会执行。也可以使用SetFinalizer(x, nil)来清理绑定到 x 上的终止器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Road int

func findRoad(r *Road) {
log.Println("road:", *r)
}

func entry() {
var rd = Road(999)
r := &rd
runtime.SetFinalizer(r, findRoad)
}

entry()
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
runtime.GC()
}