语言基础
# 函数调用
这里面涉及的东西其实挺深的,所以等我后面又时间再去研究
- Go 中函数传参仅有值传递一种方式;
- slice、map、channel都是引用类型,但是跟c++的不同;
- slice能够通过函数传参后,修改对应的数组值,是因为 slice 内部保存了引用数组的指针,并不是因为引用传递。
Golang中函数传参存在引用传递吗? - SegmentFault 思否 (opens new window)
# 匿名函数
// 这个是带返回值的
f:=func()string{
return "hello world"
}
a:=f()
// 当然我们可以直接调用
func(a int)string{
return "hello world"
}(4)
2
3
4
5
6
7
8
9
# 闭包
什么是闭包? 闭包是由函数和与其相关的引用环境组合而成的实体。
# 函数变量(函数值)
在 Go 语言中,函数被看作是第一类值,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。
func square(x int) {
println(x * x)
}
2
3
- 直接调用:
square(1)
- 把函数当成变量一样赋值:
s := square
;接着可以调用这个函数变量:s(1)
。 注意:这里square
后面没有圆括号,调用才有。
- 调用
nil
的函数变量会导致 panic。 - 函数变量的零值是
nil
,这意味着它可以跟nil
比较,但两个函数变量之间不能比较。
# 什么是闭包
先看一下这个函数,函数叫incr(),返回值为func() int
func incr() func() int {
var x int
return func() int {
x++
return x
}
}
2
3
4
5
6
7
调用这个函数会返回一个函数变量。下面是一段演示代码
func incr() func() int {
var x int
return func() int {
x++
return x
}
}
func main() {
// 获取闭包
i:=incr()
// 打印闭包
println(i()) // 1
println(i()) // 2
println(i()) // 3
// 下面返回了三个闭包
println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
首先是逃逸问题
i := incr()
:通过把这个函数变量赋值给 i
,i
就成为了一个闭包。
所以 i
保存着对 x
的引用,可以想象 i 中有着一个指针指向 x 或 i 中有 x 的地址。
由于 i
有着指向 x
的指针,所以可以修改 x
,我们可以说且保持着状态所以会出现每次打印递增的情况。此时我们可以说
那么为什么下面打印结果都是一样的呢。这是因为这里调用了三次 incr()
,返回了三个闭包,这三个闭包引用着三个不同的 x
,它们的状态是各自独立的。
# 闭包会产生的问题
现在开始通过例子来说明由闭包引用产生的问题:
地址引用的问题
x := 1
f := func() {
println(x)
}
x = 2
x = 3
f() // 3
2
3
4
5
6
7
因为闭包对外层词法域变量是引用的,所以这段代码会输出 3。可以想象 f
中保存着 x
的地址,它使用 x
时会直接解引用,所以 x
的值改变了会导致 f
解引用得到的值也会改变。
但是下面这段代码会返回1,因为我们的函数是提前调用的,所以此时已经把结果打印出来了,所以后面修改不会影响
x := 1
func() {
println(x) // 1
}()
x = 2
x = 3
2
3
4
5
6
循环闭包引用问题
每次迭代后都对 i
进行了解引用并使用得到的值且不再使用,所以下面这段代码会正常输出。
for i := 0; i < 3; i++ {
func() {
println(i) // 0, 1, 2
}()
}
2
3
4
5
然而下面这段代码会输出3
var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
f = func() {
println(i)
}
}
f() // 3
2
3
4
5
6
7
8
为啥是3呢,其实是因为i加到3才会跳出循环,此时我们打印的是i的地址,所以会打印3,但是如果我们用for range来实现,结果又不同了
var dummy [3]int
var f func()
for i := range dummy {
f = func() {
println(i)
}
}
f() // 2
2
3
4
5
6
7
8
这是因为 for range
和 for
底层实现上的不同。还有下面这个例子
var funcSlice []func()
for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
println(i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 3, 3, 3
}
2
3
4
5
6
7
8
9
10
输出序列为 3, 3, 3。
怎么解决上面的问题呢
1. 声明新变量:
- 声明新变量:
j := i
,且把之后对i
的操作改为对j
操作。 - 声明新同名变量:
i := i
。注意:这里短声明右边是外层作用域的i
,左边是新声明的作用域在这一层的i
。原理同上。
这相当于为这三个函数各声明一个变量,一共三个,这三个变量初始值分别对应循环中的 i
并且之后不会再改变。
2. 声明新匿名函数并传参:
var funcSlice []func()
for i := 0; i < 3; i++ {
func(i int) {
funcSlice = append(funcSlice, func() {
println(i)
})
}(i)
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 0, 1, 2
}
2
3
4
5
6
7
8
9
10
11
12
现在 println(i)
使用的 i
是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。
所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。
这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。
参考:
Go 语言闭包详解 (juejin.cn) (opens new window)
# go的main函数
- main函数不能带参数
- main函数不能定义返回值
- main函数所在的包必须为main包
- main函数中可以使用flag包来获取和解析命令行参数
# 接口
接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
我们可以使用结构体指针或者结构体来实现接口,但是默认情况下,还是推荐使用指针
type Cat struct {}
type Duck interface { ... }
func (c Cat) Quack {} // 使用结构体实现接口
func (c *Cat) Quack {} // 使用结构体指针实现接口
var d Duck = Cat{} // 使用结构体初始化变量
var d Duck = &Cat{} // 使用结构体指针初始化变量
2
3
4
5
6
7
8
结构体实现接口 | 结构体指针实现接口 | |
---|---|---|
结构体初始化变量 | 通过 | 不通过 |
结构体指针初始化变量 | 通过 | 通过 |
后面一些东西过于底层,所以先暂时跳过
# 值接收者和指针接收者
其实就是初始化结构体的两种方式,在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
package main
import "fmt"
type Person struct {
age int
}
func (p Person) Elegance() int {
return p.age
}
func (p *Person) GetAge() {
p.age += 1
}
func main() {
// p1 是值类型
p := Person{age: 18}
// 值类型 调用接收者也是值类型的方法
fmt.Println(p.howOld())
// 值类型 调用接收者是指针类型的方法
p.GetAge()
fmt.Println(p.GetAge())
// ----------------------
// p2 是指针类型
p2 := &Person{age: 100}
// 指针类型 调用接收者是值类型的方法
fmt.Println(p2.GetAge())
// 指针类型 调用接收者也是指针类型的方法
p2.GetAge()
fmt.Println(p2.GetAge())
}
/**
18
19
100
101
**/
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
函数和方法 | 值接收者 | 指针接收者 |
---|---|---|
值类型调用者 | 方法会使用调用者的一个副本,类似于“传值” | 使用值的引用来调用方法,上例中,p1.GetAge() 实际上是 (&p1).GetAge(). |
指针类型调用者 | 指针被解引用为值,上例中,p2.GetAge()实际上是 (*p1).GetAge() | 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 |
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
通常我们使用指针作为方法的接收者的理由:
- 使用指针方法能够修改接收者指向的值。
- 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
因而呢,我们是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质。
如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。
如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体。
接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。
# 底层实现
go的底层主要包括下面这两部分组成
type iface struct {
tab *itab
data unsafe.Pointer
}
2
3
4
tab 中存放的是类型、方法等信息。data 指针指向的 iface 绑定对象的原始数据的副本。这里同样遵循 Go 的统一规则,值传递。tab 是 itab 类型的指针。
itab 中包含 5 个字段。inner 存的是 interface 自己的静态类型。_ type 存的是 interface 对应具体对象的类型。itab 中的 _type 和 iface 中的 data 能简要描述一个变量。 _type 是这个变量对应的类型,data 是这个变量的值。这里的 hash 字段和 _type 中存的 hash 字段是完全一致的,这么做的目的是为了类型断言(下文会提到)。fun 是一个函数指针,它指向的是具体类型的函数方法。虽然这里只有一个函数指针,但是它可以调用很多方法。在这个指针对应内存地址的后面依次存储了多个方法,利用指针偏移便可以找到它们。
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
2
3
4
5
6
7
# 空 interface 数据结构
空的 inferface{} 是没有方法集的接口。所以不需要 itab 数据结构。它只需要存类型和类型对应的值即可。对应的数据结构如下:
type eface struct {
_type *_type
data unsafe.Pointer
}
2
3
4
从这个数据结构可以看出,只有当 2 个字段都为 nil,空接口才为 nil。空接口的主要目的有 2 个,一是实现“泛型”,二是使用反射。
。。。。后面的大佬直接啃到汇编去了,打扰了。。。
深入研究 Go interface 底层实现 (halfrost.com) (opens new window)
# 反射
reflect
(opens new window) 实现了运行时的反射能力,包括动态修改变量、判断类型是否实现了某些接口以及动态调用方法等功能。能够让程序操作不同类型的对象1 (opens new window)。反射包中有两对非常重要的函数和类型,两个函数分别是:
reflect.TypeOf
(opens new window) 能获取类型信息;reflect.ValueOf
(opens new window) 能获取数据的运行时表示;
func Test_question(t *testing.T) {
a:=456
fmt.Println(reflect.TypeOf(a))
fmt.Println(reflect.ValueOf(a))
/*
* int
* 456
*/
}
2
3
4
5
6
7
8
9
# 反射的三大法则
- 从
interface{}
变量可以反射出反射对象; - 从反射对象可以获取
interface{}
变量; - 要修改反射对象,其值必须可设置;
# 反射的底层与原理
数据interface中保存有结构数据,只要想办法拿到该数据对应的内存地址,然后把该数据转成interface,通过查看interface中的类型结构,就可以知道该数据的结构了
参考:
# 指针
go通过指针变量p访问成员变量的时候,有下面这两种方式来访问
p.name
(*p).name
2
为什么->不行呢,因为这个符号是用来操作管道的
- go其实是可以自动解引的,所以我们可以不使用*来获取指针,但是go解引能力有限,只能解除一次引用
- &是取地址符,放到变量前使用,就会返回相应变量的内存地址。
- *用于来获取指针的内容,指针变量可以使用这个符号来获取内容
- 结构体指针,使用 "." 操作符来访问结构体成员
- go的指针是属于引用类型,是复合类型中的一种
- go语言的指针不支持指针运算
# nil类型
Go语言中的引用类型只有五个:
切片 映射 函数 方法 通道
nil只能赋值给上面五种通道类型的变量以及指针变量。
# ... 操作符
有两个用法,一个用于函数里面 多参数 ,一个是append里面 合并 切片
这个一般用于函数拥有多个参数的情况下
下面这种方式调用是没有问题的
add([]int{1, 3, 7}...)
# 常量
单个声明
显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
2
常量枚举
const (
Unknown = 0
Female = 1
Male = 2
)
2
3
4
5
常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数
特殊常量
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
const (
a = iota
b = iota
c = iota
)
// 第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
a = iota
b
c
)
// 甚至可以这样用
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24