Golang 学习笔记(四)——复合类型、函数

Go 语言中的复合类型包括:数组(array)、切片(slice)、哈希表(map)、结构体(struct)。
函数是 Go 语言里面的核心设计。
这里结合网上的一些资料和自己的学习理解,记录一下,加深理解。

说复合类型之前先说一下指针,这样复合类型里边的一些概念就好理解了。

指针

Go 保留了指针,*T表示 T 对应的指针类型,如果包含包名, 则应该是*.T
代表指针类型的符号*总是和类型放在一起,而不是紧挨着变量名。
同样支持指针的指针**T

声明

1
var a, b *int

说明

操作符&取变量地址,用*透过指针变量间接访问目标对象
默认值是nil,没有 NULL 常量
不支持指针运算,直接’.’选择符操作指针目标对象成员
可以在 unsafe.Pointer 和任意类型指针间进行转换
可以将 unsafe.Pointer 转换为 uintptr,然后变相做指针运算,uintptr 可以转换为整数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"

type User struct {
Id int
Name string
}
func main(){
i := 100
var p *int = &i //取地址

println(*p) //取值

up := &User{1, "Jack"}
up.Id = 100 //直接取只针对想成员
fmt.Println(up)

u2 := *up //拷贝对象
u2.Name = "Tom"
fmt.Println(up, u2)
}

结果:

1
2
3
100
&{100 Jack}
&{100 Jack} {100 Tom}

Array

在 Go 语言中,数组是一个值类型(value type)。
所有的值类型变量在赋值和作为参数传递时都将产生一个复制动作。
如果作为函数的参数类型,则在函数调用时参数发生数据复制,在函数体中无法修改传入数组的内容。

声明 & 赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var VarName [n]type     // n>=0

e.g.
var a [5]int //[0 0 0 0 0]
var c [2][3]int //二维

var b int = [5]int{1,2,3,4,5} //声明并初始化

a := [3]int{1,2,3}
b := [10]int{1,2,3} //前三个元素,其他为0
c := [20]int{19:1} //第20个元素初始化为1,其他默认0
d := [...]int{4,5,6} //自动计算长度
e := [...]int{0:1, 1:2, 19:3} //自动推断


// 二维数组
doubleArray := [2][4]int{[4]int{1,2,3,4}, [4]int{5,6,7,8}}
easyArray := [2][4]int{{1,2,3,4}, {1,2,3,4}}
// 多维 [...][n] 前者可推断,但是后者必须显示赋值

数组的长度是该数组类型的一个内置常量

1
arrLength := len(arr)

注意,数组长度也是类型的一部分,因此不同长度数组为不同类型(内置常量)

[3]int[4]int是不同类型,并且数组不能改变长度

数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本(一次复制操作),而不是它的指针,如果要传入指针,使用slice

元素访问

1
2
3
4
5
6
7
for i:=0; i < len(array); i++ {
fmt.Println(i, array[i])
}

for i, v := range array {
fmt.Println(i, v)
}

可以用new创建数组, 返回一个指向数组的指针

1
p := new([10]int)

注意区分

1
2
3
4
5
6
7
// 指向数组的指针
a := [100]int{}
var p *[100]int = &a

// 指针数组
x, y = 1, 2
a := [...]*int{&x, &y}

Slice

切片(类似 Java 中的 ArrayList)就像一个指向数组的指针,但更复杂,实际上它拥有自己的数据结构,而不仅仅是指针(指向原生数组的指针 + 数组切片中元素个数 + 数组切片已分配的存储空间)。
切片是一个引用类型,总是指向一个底层 array,声明可以向 array 一样,只是不需要长度。
slice 就像一个结构体,包含三个元素:

  • 一个指针,指向数组中 slice 指定的开始位置
  • 长度,即 slice 的长度
  • 最大长度,也就是 slice 开始位置到数组的最后位置的长度

声明 & 赋值

通过 array 创建

1
2
3
4
5
6
7
var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
var mySlice []int = myArray[:5]

a := [5]int{1,2,3,4,5}
b := a[2:4]
b := a[:4]
b := a[2:]

从数组或已存在的 slice 再次声明

1
2
3
4
5
var ar [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

var a, b []byte
a = ar[2:5]
b = ar[3:5]

直接创建

1
2
3
myslice1 := make([]int, 5)
myslice2 := make([]int, 5, 10) //初始个数5,预留10个元素的存储空间
myslice3 := []int{1,2,3,4,5}

元素访问

1
2
3
4
5
6
7
for i:=0; i<len(mySlice); i++ {
fmt.Println(i, mySlice[i])
}

for i, v := range mySlice {
fmt.Println(i, v)
}

其他操作

大小和容量

len获取 slice 的长度
cap获取 slice 的最大容量
容量需要注意一下,数组值的容量总是等于其长度。而切片值的容量则往往与其长度不同。请看下图。

动态增减元素

append向 slice 里面追加一个或者多个元素,然后返回一个和 slice 一样类型的 slice

1
2
3
//append
mySlice = append(mySlice, 1, 2, 3) //增加三个元素
mySlice = append(mySlice, mySlice2) //增加另一个

特别注意(这是一个大坑):
append会改变 slice 所引用的数组的内容,从而影响到引用统一数组的其他 slice,但当 slice 中没有剩余空间,此时动态分配新的数组空间返回的 slice 数组指针将指向这个空间,而原数组的内容将保持不变,其他引用此数组的 slice 不受影响

内容复制

可以用copy从源 slice 的 src 中复制到目标 dst,并且返回复制元素的个数。

1
2
3
4
5
6
7
copy(dst, source) //会按短的个数复制

slice1 := []int{1,2,3,4,5}
slice2 := []int{5,4,3}

copy(slice2, slice1) //复制slice1前三个 1 -> 2
copy(slice1, slice2) //复制slice2的前三个 2 -> 1

切片

默认开始位置 0,ar[:n]等价于ar[0:n]
第二个序列默认是数组长度ar[n:]等价于ar[n:len(ar)]
从一个数组直接获取 slice,可以是ar[:]
slice 是引用类型,所以当改变其中元素的时候,其他的所有引用都会改变。

1
2
aSlice = array[3:7]
bslice = aSlice[:3]

Map

Map 类型其实是哈希表的一个实现,类似 Java 中的 HashMap、Python 中字典的概念。
map 是无序的,长度不固定,内置的len可以用于 map,可以方便的修改。

声明 & 赋值

初始化一个 Map

1
2
3
4
5
6
7
8
9
map[keyType]valueType

var m map[string] PersonInfo
m = make(map[string] personInfo[, 100])

var numbers map[string]int
or
numbers := make(map[string]int)
numbers["one"] = 1

元素访问

1
2
3
4
5
6
7
8
rating := map[string]float32 {"c":5, "Go":4.5}

csharpRating, ok := rating["C#"]
if ok {
fmt.Println("get the value")
} else{
fmt.Println("error")
}

基本操作

赋值

1
m["1234"] = PersonInfo{}

删除

1
delete(m, "1234")

struct

struct,一组字段的集合,类似其他语言的 class。
Go 放弃了大量包括继承在内的面向对象特性,只保留了组合(composition)这个最基础的特性。

声明及初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type person struct {
name string
age int
}
//初始化

func main() {
var P person

P.name = "tom"
P.age = 25
fmt.Println(P.name)

P1 := person{"Tom1", 25}
fmt.Println(P1.name)

P2 := person{age: 24, name: "Tom"}
fmt.Println(P2.name)
}

struct 的匿名字段(嵌入字段)

这是实现类似于继承的一种手段,但是其实不同于继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Human struct {
name string
age int
weight int
}

tyep Student struct {
Human //匿名字段,默认Student包含了Human的所有字段
speciality string
}

mark := Student(Human{"mark", 25, 120}, "Computer Science")

mark.name
mark.age

能够实现字段继承,当字段名重复的时候,优先取外层的,可以通过指定 struct 名还决定取哪个

1
2
mark.Human = Human{"a", 55, 220}
mark.Human.age -= 1

struct 不仅可以使用 struct 作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应字段上做函数操作

method

1
2
3
4
5
6
7
8
9
type Rect struct {
x, y float64
width, height float64
}

//method
func (r ReciverType) funcName(params) (results) {

}

Reciver 默认以值传递,而非引用传递,还可以是指针。
指针作为 Receiver 会对实例对象的内容发生操作,而普通类型作为 Receiv er 仅仅是以副本作为操作对象,而不对原实例对象发生操作。

如果一个 method 的 receiver 是*T,调用时,可以传递一个T类型的实例变量V,而不必用&V去调用这个 method

1
2
3
4
5
6
7
func (r *Rect) Area() float64 {
return r.width * r.height
}

func (b *Box) SetColor(c Color) {
b.color = c
}

method 继承和重写

采用组合的方式实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Human struct {
name string
}

type Student struct {
Human
School string
}

func (h *Human) SayHi() {
fmt.Println(h.name)
}

//则Student和Employee的实例可以调用
func main() {
h := Human{name: "human"}
fmt.Print(h.name)
h.SayHi()

s := Student{Human{"student"}}
s.SayHi()

}

还可以进行方法重写

1
2
3
4
func (e *Student) SayHi() {
e.Human.SayHi()
fmt.Println(e.School)
}

make 和 new

make用于内建类型(map, slice, channel) 的内存分配。

new用于各种类型的内存分配。new本质上和其他语言中同名函数一样,new(T)分配了零值填充的T类型的内存空间,并返回其地址,即一个*T类型的值 即,返回一个指针,指向新分配的类型T零值

make(T, args),只能创建 slice、map、channel,并返回一个有初始值(非零值)的T类型,而不是*T。 本质来讲,导致这三个类型有所不同的原因是,指向数据结构的引用在使用前必须被初始化。

函数

函数是 Go 语言里面的核心设计,通过关键字func来声明

1
2
3
4
func funcName(input type1, input2 type2) (output1 type1, output2 type2) {
//logical code
return value1, value2
}

基本语法

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//一般函数
func func_name(a int) {
println(a)
}

//多参数,无返回值
func func_name(a, b int, c string) {
println(a, b, c)
}

//单个返回值
func func_name(a, b int) int { //同类型,可以省略 a, b int
return a + b
}

//多个返回值
func func_name(a, b int) (c int, err error) { //返回值还可以是 (int, error)
return a+b, nil
}

func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}

说明

Go 函数通过关键字func声明
可以有一个或多个参数,每个参数后面带有类型。通过,分隔函数可以返回多个值
返回值声明,可以只声明类型。
如果没有返回值,可以省略最后的返回信息;如果有返回值,必须在外层添加 return。

不支持:

  • 嵌套(nested)
  • 重载(overload)
  • 默认参数(default parameters)

支持:

  • 无需声明原型
  • 不定长度变参
  • 多返回值
  • 命名返回值参数
  • 匿名函数
  • 闭包

注意:

  • 函数使用func开头,左大括号不能另起一行
  • 小写字母开头的函数指在本包内可见,大写字母开头的函数才能被其他包调用

多返回值

可以像 python 那样返回多个结果,只是非 tuple。对于不想要的返回值,可以扔垃圾桶_

如果用命名返回参数,return 语句可以为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

func change(a, b int) (x, y int) {
x = a + 100
y = b + 100

return //101, 102
//return x, y //同上
//return y, x //102, 101
}

func main(){
a := 1
b := 2
c, d := change(a, b)
println(c, d)

如果命名返回参数被代码块中的同名变量覆盖了,就必须使用显式 return 返回结果。
不需要强制命名返回值,但是命名后的返回值可以让代码更加清晰,可读性更强。

参数传递

传值与传指针

指针, Go 保留指针,用.而非”->”操作指针目标对象成员。
操作符:
&取变量地址
*通过指针间接访问目标函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func add1(a int) int {
a = a + 1
return a
}

x := 3
x1 := add1(x)
x //3
x1 //4
传值,x1的值没有改变

func add2(a *int) int {
*a = *a + 1
return *a
}
x := 3
x1 := add2(&x)
x // 4
x1 // 4

传指针的好处:

  1. 传指针多个函数能操作同一个对象;
  2. 传指针比较轻量级(8byte),只是传内存地址,我饿们可以用指针来传递体积大的结构体。

Go 语言中,string、slice、map 这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传指针
注意,若函数需要改变 slice 长度,仍需要取地址传指针

可变参数

变参本质上就是一个 slice,且必须是最后一个形参。
将 slice 传递给变参函数时,注意用展开(类似 Java 中的可变参数),否则会被当做单个参数处理,和 python 类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

func sum(s string, args ...int) {
var x int
for _, n := range args {
x += n
}
println(s, x)
}
func main(){
sum("1+2+3=", 1, 2, 3)

x := []int{0,1,2,3,4}
sum("0+1+2+3=", x[:4]...)
}

...type类型只能作为函数的参数类型存在,并且是最后一个参数
本质上是一个数组切片,即[]type

传入任意类型的不定参数:

1
2
func Printf(format string, args ...interface{}) {
}

匿名函数

1
2
3
f := func(x,y int) int {
return x + y
}

函数作为值、类型

在 Go 语言中,函数也是一种变量,可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的函数。

语法:

1
type typeName func (input1 inputType1, input2 inputType2 [, ....]) (result1 resultType1 [,....])

用法 1 (这种用法,在写接口的时候非常有用)
e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type testInt func(int) bool //声明了一个函数类型

func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
}

func isOdd(integer int) bool {
if integer % 2 == 0 {
return false
}
return true
}

filter(a, isOdd)

用法 2 可以定义函数类型,也可以将函数作为值进行传递(默认值nil
e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

//定义函数类型callback
type callback func(s string)

//定义一个函数,可以接收另一个函数作为参数
// sum为参数名称, func(int, int) int为参数类型
func test(a, b int, sum func(int, int) int) {
println( sum(a,b) )
}

func main(){
//演示1
var cb callback
cb = func(s string) {
println(s)
}
cb("hello world")

//演示2
test(1, 2, func(a, b int) int {return a + b})
}

结果:

1
2
hello world
3

main函数和init函数

Go 里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

Go 程序会自动调用init()main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

参考资料:
谢大的《Go Web 编程》
wklken 的 Golang 笔记


Golang 学习笔记(四)——复合类型、函数
https://www.haoyizebo.com/posts/fee45177/
作者
一博
发布于
2016年1月6日
许可协议