Golang复合类型大致分为数组、切片、map、结构体、指针等。

命名类型和类型别名

  • 命名类型type newType oldType,newTypeoldType是不同类型,只是底层类型相同。

  • 类型别名type alias = oldTypenewTypeoldType是相同类型。

底层类型决定了类型支持的操作。

1
2
3
4
5
6
7
8
9
type myInt int
type myAlias = int
var myIntVal myInt = 5
var myAliasVal myAlias = 5
var intVal = 5
fmt.Printf("%T %T %T\n", myIntVal, myAliasVal, intVal) //main.myInt int int
//fmt.Println(myIntVal == myAliasVal)                  //compile error, 不同类型不能比
fmt.Println(myIntVal == myInt(myAliasVal)) //true,底层类型相同可以转换
fmt.Println(myAliasVal == intVal)          //true,相同类型可以比较

数组

数组创建方式

  • 只声明不初始化:var arrName [n]elementType

  • 声明并使用字面值初始化:arr := [n]elementType{element1, element2, element3..., elementn}

  • 声明并使用字面值初始化,且数量由实际元素个数推导而来:arr:= [...]elementType{element1, element2, element3},或者用arr:= [...]elementType{1:element1, 3:element3}的方式。

特点

  • 数组的长度是固定的。

  • 数组是值类型,发生拷贝的时候会将所有元素拷贝一遍。

  • 数组的长度是数组类型的一部分,[5]int[10]int是不同的类型。

数组示例

1
2
3
4
5
6
7
arr0 := [0]int{}
arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}
arr3 := [...]int{2: 3}

fmt.Printf("%T %T %T %T\n", arr0, arr1, arr2, arr3)                     //[0]int [3]int [3]int [3]int
fmt.Printf("%d %d %d %d\n", len(arr0), len(arr1), len(arr2), len(arr3)) //0 3 3 3

注意

省略号只能作为使用数组字面值初始化时推导字面值的长度,不能作为数组类型的一部分,即var arr4 [...]int = [...]int{1, 2, 3}的写法是错误的。

切片

切片的本质是一个数据结构,其底层数据结构仍然是一个数组,但是通过封装的一些操作,让我们使用起来像是一个可变长度的数组。

%GOROOT%\src\runtime\slice.go中可以看到切片的定义:

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

这里的Pointer是指向切片第一个元素对应到底层数组中的元素的指针而非底层数组第一个元素的指针,lencap分别表示当前切片的长度和容量。

切片创建方式

  • 通过已有数组创建切片s := array[m:n]表示创建一个切片,其底层数组为array,第一个元素为array[m],长度为n - m,这里的m和n都可以省略,如果m省略,则默认值为0,如果n省略,则默认值为len(array) - 1

  • 通过已有切片创建切片s2 := s1[m:n]表示创建一个切片,其底层数组和s1相同,第一个元素为s1[m],长度为n - m,这里的m和n同样可以省略。

  • 使用内置函数make创建,s := make([]elementType, len, cap),其中的lencap都可以省略。

  • string类型可以通过强制转换直接创建对应的[]byte或者[]rune

切片支持操作

  • len(s)返回切片长度。

  • cap(s)返回切片容量。

  • s = append(s, 1, 2, 3)向切片中追加元素,这里的参数时变长参数,可以使用s = append(s, s1...)的方式将一整个切片转换成变长参数使用。

  • copy(to, from)from切片中的内容复制到to切片中,复制的实际长度为min(len(from), len(to))

切片示例

 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
arr := [...]int{1, 2, 3, 4, 5}
//通过已有数组创建
s1 := arr[:]
fmt.Printf("%d %d\n", len(s1), cap(s1)) //5  5
//通过已有切片创建
s2 := s1[:3]
fmt.Printf("%d %d\n", len(s2), cap(s2)) //3  5
//通过make方法创建,自动创建底层数组
s3 := make([]int, 3, 5)
fmt.Printf("%d %d\n", len(s3), cap(s3)) //3  5
//通过对string强转创建,底层数组就是string的内存
str1, str2 := "hello", "world"
s4 := []rune(str1)
fmt.Printf("%d %d\n", len(s4), cap(s4)) //5  5
s5 := []byte(str2)

//共享内存的两个切片,修改其中一个会导致另一个的变化
fmt.Printf("%d %d\n", s1[0], s2[0]) //1 1
s1[0] = -1
fmt.Printf("%d %d\n", s1[0], s2[0]) //-1 -1

//使用append追加元素,如果底层素组长度不够会自动转移到新内存
s6 := append(s1, 6)
fmt.Printf("%d %d\n", len(s1), len(s6)) // 5 6
fmt.Printf("%d %d\n", cap(s1), cap(s6)) // 5 10
fmt.Printf("%p %p\n", &s1[0], &s6[0])   //0xc00000a420 0xc000010230

//内置函数copy只会拷贝两个切片中长度最少的那个切片的长度
fmt.Println(s1)           //[-1 2 3 4 5]
fmt.Println(s3)           //[0 0 0]
fmt.Println(copy(s3, s1)) //3
fmt.Println(s1)           //[-1 2 3 4 5]
fmt.Println(s3)           //[-1 2 3]

//string和[]rune、[]byte可以互转,但是不会影响原string,实际是在转换的时候重新分配了内存
s4[0] = 'H'
fmt.Println(str1)       //hello
fmt.Println(string(s4)) //Hello
s5[0] = 87
fmt.Println(str2)       //world
fmt.Println(string(s5)) //World

map

map创建方式

  • 只声明不初始化var m map[KeyType][ValueType]

  • 使用字面值初始化m := map[string]int{"a":1, "b":2}

  • 使用make方法创建var m map[string]int = make(map[string]int, len)

map操作方式

  • 使用map的key值访问其value值v,ok := m["a"] m["b"] = 5

  • 通过range方式去遍历,每次顺序会不一样。

  • 通过内置函数delete(m, "a")删除元素。

  • 通过len(m)访问map包含键值对数量。

map特点

  • 一个map类型变量是对map的引用,一个未创建的map是nil。

  • 两个map不能进行比较,但是可以和nil进行比较。

  • 可以安全地对未创建的map进行查找、delete、len和range操作,但是存入元素会导致panic。

  • map中的value并不是一个变量,不能对其进行取地址操作。

  • 如果map中保存的value是一个结构体,不能通过m[k].xxx = xxx的方式修改value的值,但是如果是指针或者切片可以。

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
m1 := map[string]int{
    "a": 1,
    "b": 2,
    "c": 0,
}
var m2 map[string]int
var m3 map[string]int = map[string]int{}
fmt.Println(m1, m2, m3) //map[a:1 b:2 c:0] map[] map[]
//fmt.Println(m1 == m2)//compile error
//fmt.Println(&m1["a"])//compile error
fmt.Println(m1 == nil)                              //false
fmt.Println(m2 == nil)                              //true,m2未初始化,是nil
fmt.Println(m3 == nil)                              //false,m3是空map,但是不是nil
fmt.Printf("%d %d %d\n", m1["a"], m2["a"], m3["a"]) //1 0 0
fmt.Printf("%d %d %d\n", len(m1), len(m2), len(m3)) //3 0 0
//delete安全
delete(m1, "a")
delete(m2, "a")
delete(m3, "a")
//range安全
for k, v := range m1 {
    fmt.Printf("%s:%d ", k, v) //b:2 c:2
}
fmt.Println()
for k, v := range m2 {
    fmt.Printf("%s:%d ", k, v)
}
fmt.Println()
for k, v := range m3 {
    fmt.Printf("%s:%d ", k, v)
}
fmt.Println()

不能直接修改map value的成员的值。

1
2
3
4
5
6
7
8
9
color1 := color.RGBA{1, 0, 0, 1}
color2 := color.RGBA{0, 1, 0, 1}
color3 := color.RGBA{0, 0, 1, 1}
colorMap := map[string]color.RGBA{
    "color1": color1,
    "color2": color2,
    "color3": color3,
}
//colorMap["color1"].A = 0 //compile error

但是改成指针可以。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
color1 := color.RGBA{1, 0, 0, 1}
color2 := color.RGBA{0, 1, 0, 1}
color3 := color.RGBA{0, 0, 1, 1}
colorMap := map[string]*color.RGBA{
    "color1": &color1,
    "color2": &color2,
    "color3": &color3,
}
colorMap["color1"].A = 0
fmt.Println(color1)              //{1 0 0 0}
fmt.Println(*colorMap["color1"]) //{1 0 0 0}

这里大胆猜测因为Map的value是通过值拷贝的方式保存的,也就是说在保存非指针的情况下,colorMap中键值"color1"对应的value是变量color1的一份拷贝,如果可以对其进行修改,其实这种修改不会影响到color1变量本身,容易造成误解,所以Golang禁止了这种操作,而使用指针类型,即使是值拷贝,两个value保存的也仍然是同一份地址,对应的都是color1变量的地址,不会造成误解。

结构体

结构体定义方法

  • 直接使用结构体字面值:
1
2
3
4
st1 := struct {
    Name   string
    Number int
}{Name: "name1", Number: 1}
  • 使用命名类型:
1
2
3
4
type UserInfo struct {
    Name          string
    Number, Score int
}

结构体变量声明方式

  • 按照结构体内成员声明顺序依次赋值,不灵活:
1
userInfo := UserInfo{"name", 2, 99}
  • 通过按照成员命名方式赋值,推荐:
1
userInfo := UserInfo{Number: 2, Name: "name"}//可以部分赋值,未赋值的成员使用默认值

结构体示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
st1 := struct {
    Name   string
    Number int
}{Name: "name1", Number: 1}
type UserInfo struct {
    Name          string
    Number, Score int
}
st2 := UserInfo{Name: "name2", Number: 2} //部分赋值
st3 := struct {
    Name   string
    Number int
}{Name: "name3", Number: 3}
st4 := struct {
    Number int
    Name   string
}{Name: "name4", Number: 4}
fmt.Printf("%T %T %T %T\n", st1, st2, st3, st4)         //struct { Name string; Number int } main.UserInfo struct { Name string; Number int } struct { Number int; Name string }
fmt.Println(reflect.TypeOf(st1) == reflect.TypeOf(st3)) //true
fmt.Println(reflect.TypeOf(st1) == reflect.TypeOf(st4)) //false

指针

指针声明

  • var p *int = nil

  • 多级指针var p **int = nil

指针操作

  • 解引用n := *p

  • 访问结构体成员*p.Field = xxx

  • 不允许指针计算。

  • 允许在函数中通过指针返回局部变量的地址,这种情况下编译器会自动将局部变量的内存空间分配到堆上。