0%

Go学习笔记-函数

前言

本章来了解下golang中函数的使用。

基本使用

golang中定义函数使用func关键字,实例如下:

1
2
3
4
func helloWorld(param int) (result,err){
// TODO anything
return nil
}

与Java中的方法的结构大同小异,都是由函数名,入参,返回类型,方法体构成。
不过golang中支持多个不同类型的返回结果,这一点值得注意。

参数传递

参数传递是函数不可跳过的一个话题,虽然参数传递分为值传递和引用传递,不过我们都知道引用传递的本质还是
值传递,只不过因为指针的特殊性导致其表现效果与值传递不一样,也就是方法体内对参数的修改会影响原值,所
以才单独归为一类。

golang与Java一样,参数的传递也是值传递,下面通过实例来验证下。

基本类型/string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func BaseData(num1 int, num2 float32, str string) {
num1 = num1 + 1
num2 = 3.2
str = "hello"
fmt.Printf("after change---num1:%v,num2:%v,str:%v\n", num1, num2, str)
}
func main() {
num1 := 1
var num2 float32 = 2.3
str := "kkll"
functest.BaseData(num1, num2, str)
fmt.Printf("result------num1:%v,num2:%v,str:%v\n", num1, num2, str)
}
// 结果输出:
// after change---num1:2,num2:3.2,str:hello
// result------num1:1,num2:2.3,str:kkll

基本类型和string类型不用多说,值传递无疑,方法体内的修改不会影响到原值。

数组/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
func ArrayData(array [3]int, sliceParam []int, arrayPoint [3]*int) {
array[1] = 1
fmt.Printf("array change:%v\n", array)
sliceParam[0] = 0
sliceParam = append(sliceParam, 10)
fmt.Printf("slice change:%v\n", sliceParam)
num := 11
arrayPoint[0] = &num
fmt.Printf("arrayPoint change:%v\n", *arrayPoint[0])
}
func main() {
array := [3]int{1, 2, 3}
sliceParam := make([]int, 0)
sliceParam = append(sliceParam, 1, 2, 3)
num := 1
arrayPoint := [3]*int{&num}
functest.ArrayData(array, sliceParam, arrayPoint)
fmt.Printf("array:%v,slice:%v,arrayPoint:%v\n", array, sliceParam, *arrayPoint[0])
}
// 结果输出
// array change:[1 1 3]
// slice change:[0 2 3 10]
// arrayPoint change:11
// array:[1 2 3],slice:[0 2 3],arrayPoint:1

数组这里也是值传递没啥问题,数组元素为指针类型也一样,传递的是整个数组的副本。
切片就有点不同了,首先,切片是对某一段数组的引用,他的定义如下:

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

slice本质上是一个结构体,按照值传递理解,这里会传一个这个结构体的副本,也就是这里的array指针,len,
cap都会复制一份。
那么这里对数组片段的引用是不变的,所以这也解释了为什么示例中,修改切片元素值会导致原切片对应元素的
修改,但使用append方法却不会,因为原切片引用的片段不变,append新加元素对于原切片来说不存在。

map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func MapData(map1 map[int]string, map2 map[int]*string) {
map1[0] = "first"
map1[10] = "changed"
str := "jjkjk"
map2[0] = &str
fmt.Printf("map1:%v,map2:%v\n", map1, map2)
}
func main() {
map1 := make(map[int]string, 0)
map2 := make(map[int]*string, 0)
map1[10] = "change"
functest.MapData(map1, map2)
fmt.Printf("map1:%v,map2:%v\n", map1, map2)
}
// 结果输出:
// map1:map[0:first 10:changed],map2:map[0:0xc00016ed00]
// map1:map[0:first 10:changed],map2:map[0:0xc00016ed00]

可以看到,map与slice不同,不论是对原有元素的修改还是新增元素都会改变原map集合。
这里先看看map的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// A header for a Go map.
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
// mapextra holds fields that are not present on all maps.
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8
}

这里map中无论是代表桶的buckets还是存储键值对的mapextra结构,都是用的指针类型,所以传入map参数
时,其副本保存的也是这些指针的值,自然对map元素的修改或新增元素多会影响到原map集合。

struct

其实在看了slice和map的传参示例后,对struct的传参表现已经有所预期了,不过这里还是看一个示例,眼见为实:

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
58
59
60
61
62
63
type Father struct {
Name string
Age int
Wife *string
ChildOne Child
ChildTwo *Child
}

type Child struct {
Name string
}

func StructData(father Father, father2 Father) {
father.Name = "qwq"
father.Age = 11
wife := "xvxvxv"
father.Wife = &wife
father.ChildOne = Child{
Name: "newOne",
}
father.ChildTwo = &Child{
Name: "newTwo",
}
father2 = Father{
Name: "mmkmk",
Age: 12,
}
fmt.Printf("father1:%+v,father2:%+v\n", father, father2)
}
func main() {
wife := "jkjkll"
father := functest.Father{
Name: "jjjj",
Wife: &wife,
ChildOne: functest.Child{
Name: "one",
},
ChildTwo: &functest.Child{
Name: "two",
},
}
father2 := functest.Father{
Name: "eee",
ChildOne: functest.Child{
Name: "one",
},
ChildTwo: &functest.Child{
Name: "two",
},
}
fmt.Printf("father1:%+v,father2:%+v\n", father, father2)
functest.StructData(father, father2)
fmt.Printf("father1:%+v,father2:%+v\n", father, father2)
}
// 结果输出:
// father1:{Name:jjjj Age:0 Wife:0xc00016ed00 ChildOne:{Name:one} ChildTwo:0xc00016ed10},
// father2:{Name:eee Age:0 Wife:<nil> ChildOne:{Name:one} ChildTwo:0xc00016ed20}

// father1:{Name:qwq Age:11 Wife:0xc00016ed30 ChildOne:{Name:newOne} ChildTwo:0xc00016ed40},
// father2:{Name:mmkmk Age:12 Wife:<nil> ChildOne:{Name:} ChildTwo:<nil>}

// father1:{Name:jjjj Age:0 Wife:0xc00016ed00 ChildOne:{Name:one} ChildTwo:0xc00016ed10},
// father2:{Name:eee Age:0 Wife:<nil> ChildOne:{Name:one} ChildTwo:0xc00016ed20}

不出所料,struct类型的参数传递仍然是传递副本,所以方法体内的修改不影响原struct的值。

小结

这里就不再看指针类型的示例了,小结一下:golang中参数传递的本质仍然是值传递,只不过因为一些数据结构
包含指针,导致这些数据类型的传参会是引用传递。所以这里只要把握住值传递并了解各个数据类型的结构,例
如map的结构,就能准确判断每种数据类型在传参中的表现。

方法

方法可以理解为有归属对象的函数,例如Java中的方法都属于某一个类对象。
golang中也有方法的概念,不过定义上与Java大不相同,下面看一个例子:

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
type Owner struct {
Text string
}

func (o *Owner) Show(text string) {
o.Text = text
fmt.Printf("point-Owner:%v\n", *o)
}

func (o Owner) Show2(text string) {
o.Text = text
fmt.Printf("no-point-Owner:%v\n", o)
}
func main() {
owner := functest.Owner{
Text: "111",
}
owner.Show("jjjj")
fmt.Printf("owner:%v\n", owner)
owner.Show2("xxxx")
fmt.Printf("owner2:%v\n", owner)
}
// 结果输出:
// point-Owner:{jjjj}
// owner:{jjjj}
// no-point-Owner:{xxxx}
// owner2:{jjjj}

通过将某个函数归属到某个结构体类型下,也就是指定函数的接收者,使之成为这个结构体的一个方法。

golang的方法直接在定义时就声明了一个接收者的对象(如:func (o Owner) show() 中的o),所以golang中
没有this这种指针,而是直接使用定义好的对象实例。而根据实例定义时是否使用了指针类型,可以决定方法是否
能影响到原实例的值。

另外,方法的接收者并不限于结构体类型:

1
2
3
4
5
type NewInt int

func (i NewInt) Add(){
i = i+1
}

使用type关键字定义的内置类型也可以作为方法的接收者。

组合与继承

golang中的结构体没有Java中类对象继承,实现这些功能。
不过结构体可以通过组合的方式变相实现这种关系。

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
type Owner struct {
Text string
}

func (o *Owner) Show(text string) {
o.Text = text
fmt.Printf("point-Owner:%v\n", *o)
}

func (o Owner) Show2(text string) {
o.Text = text
fmt.Printf("no-point-Owner:%v\n", o)
}

type Boss struct {
Owner
Text string
}

func (b Boss) Show(text string) {
b.Text = text
}
func main() {
owner := functest.Owner{
Text: "1212",
}
boss := functest.Boss{
Owner: owner,
Text: "llkk",
}
boss.Show("xixix")
fmt.Printf("boss.owner.text:%v\n", boss.Owner.Text)
boss.Owner.Show("jljl")
boss.Show2("jjj")
}
// 结果输出如下:
// boss.owner.text:1212
// point-Owner:{jljl}
// no-point-Owner:{jjj}

当两个结构体形成组合的关系时,外部的结构体,就可以访问内部结构体的属性与方法。
特别是使用匿名属性时,可以省略内部结构体名称直接调用,达到类似继承中子类调用父类方法的效果。
而如果有同名的方法,属性时,就会访问到外部结构体自己的属性与方法。

看起来像是继承中子类同名方法覆盖父类方法,但本质是不同的,外部结构仍然可以通过他的结构体属性访问到该属性拥有的方法。
所以需要注意这里仍然是两个结构体的组合关系,不能与继承混为一谈。

接口

golang中的接口是一种内置类型,代表一组抽象方法。而且与Java不一样的是,不需要显示声明实现,只要有
结构体拥有同名的方法就算是实现了该接口的抽象方法。
下面来看一下具体例子:

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
type Method interface {
PpMm(s string)
}

type Pm struct {
Text string
}

func (p Pm) PpMm(tt string) {
p.Text = tt
fmt.Printf("p的tt:%v\n", p)
}

type NoPm struct {
Str string
}

func (n NoPm) NoPpMm(ss string) {
n.Str = ss
fmt.Printf("noPm:%v\n", n)
}
func main() {
pm := functest.Pm{
Text: "sss",
}
noPm := functest.NoPm{
Str: "ttt",
}
noPm.NoPpMm("jl")
var infe functest.Method
infe = pm
infe.PpMm("kk")
fmt.Printf("interface:%v\n", infe)
//infe = noPm 无法通过编译
}
// 结果输出:
// noPm:{jl}
// p的tt:{kk}
// interface:{sss}

这里可以看到,拥有与接口同名方法的结构体Pm自动与接口Method形成了实现的关系,Pm可以自动转成interface类型。
但没有同名方法的NoPm则不行。

总结

golang的函数与Java的方法在声明定义,参数传递上都比较相似,很容易理解。
不过因为没有Java中class,继承,实现这些概念,所以golang中的方法和接口就与Java的大不一样。
不过从使用上来来看,这种设计确实更加简单灵活,没有Java那么紧密耦合的关系。

下一章

Go学习笔记-for和range