Go语言入门基础(四)——数据类型(复合数据类型)

123 阅读8分钟

阅读本文前,建议已掌握基本C语言语法及规范,本文将更加侧重Go语言和C系列语言的区别以达到快速上手的目的

本文主要参考《Go语言圣经(中文版)》及个人实践撰写,具体可参考: Go语言圣经

本文作为该系列第四篇,将重点介绍数据类型,上一篇介绍了基础数据类型,本篇将介绍复合数据类型

复合数据类型的“复合”之处在于其由基础数据类型的元素组成,C语言中聚合类型(如结构体、枚举、共用体等)就是复合数据类型的一种。Go语言的复合数据类型具体分为数组/切片(slice)/字典(map)/结构体四种。

数组

数组由相同数据类型元素构成的聚合类型。Go语言中数组和C语言几乎相同,具有固定长度索引访问的特性,其中索引也是从0开始。

可以用以下标准语句声明并初始化一个数组:

var 数组名 [数组大小] 元素数据类型 = [数组大小] 元素数据类型 {初始化值}

参照变量的极简声明语句,也可以写出数组的极简声明(即数组的第二种声明方式):

数组名 := [数组大小] 元素数据类型 {初始化值}

参考以下代码段,首先数组两种方式的声明及整体输出:

package main
import "fmt"

func main() {
	var s1 [3]int = [3]int{1, 2, 3}   //普通声明
	s2 := [3]string{"1", "2", "3"}    //极简声明
	fmt.Println(s1)
	fmt.Println(s2)
}

运行结果如下: image.png

由于Go语言的零值初始化性质,如果在声明时不手动初始化,自动初始化为当前数据类型的零值,参考以下代码及运行结果:

package main
import "fmt"

func main() {
	var s1 [3]int
	var s2 [3]string
	fmt.Println(s1)
	fmt.Println(s2)
}

image.png

在极简声明中可以进一步简化——将元素个数省去,使用...替代,编译器将根据初始化的元素个数,自动推得数组大小,参考以下代码及运行结果:

package main

import "fmt"

func main() {
	s1 := [...]int{1, 3, 5, 7, 9}
	fmt.Println(s1)
	fmt.Printf("%T\n", s1)
}

上述代码中占位符&T代表相应值的类型的Go语法表示,可显示数组大小及类型,运行结果如下: image.png

除了使用占位符展现数组信息,还可以用内置的len函数求出数组长度。参考以下代码:

package main

import "fmt"

func main() {
	s1 := [...]int{1, 3, 5, 7, 9}
	fmt.Println(len(s1))
	fmt.Println(s1[len(s1)-1])   //打印末尾元素
}

运行结果如下,可以看到可以利用len求得的长度信息对应到最后一个元素的索引: image.png

切片(Slice)

切片和数组一样,是同种数据类型元素聚合而成的“聚合类型”,但是数组的不可变长度性既会导致某些情况下空间不足,也会导致某些情况下空间浪费。所以Go语言中内置了可变长度的Slice类型。

定义切片

切片的定义有两种方式:

  1. 使用未指定大小的数组定义切片,标准语句如下:
var 切片名 [] 数据类型
  1. 使用内置的make函数创建切片,标准语句如下:
var 切片名 [] 数据类型 = make([]数据类型, 初始长度)   //普通声明
切片名 := make([]数据类型, 初始长度)                 //极简声明

其中make函数通常使用两个参数typelen(如上),第三个参数capacity(容量)可选。

容量问题(capacity)

虽然将切片类型区别于数组提出,但是每一个切片对象都有一个底层数组(或多个切片对象共享一个底层数组)。所以虽然切片类型是一个长度可变的数据类型,但是底层也有一定的容量限制。

取《Go语言圣经》中的例子: image.png

数组months是底层数组,切片可以取其中部分,如切片Q2、summer等。

参考定义切片中的make函数,定义声明时可固定容量。当后续不断追加元素,超过容量时,容量会自动翻倍。可以通过内置函数len获取切片长度、函数cap获取底层数组容量。

参考以下代码,实现简单的容量翻倍:

package main

import "fmt"

func main() {
	sli := make([]int, 5, 5) //初始长度为5,容量为5
	fmt.Println(sli)
	fmt.Printf("length: %d\ncapacity: %d\n", len(sli), cap(sli))
	sli = append(sli, 2333) //追加一个元素
	fmt.Println(sli)
	fmt.Printf("length: %d\ncapacity: %d\n", len(sli), cap(sli))
}

运行结果如下,可以看到容量cap由原来的5翻倍为10: image.png

初始化

① 声明时直接初始化:

切片名 := [] 数据类型 {初始化内容}
//例如:
sli := [] int {1,2,3}    // cap=len=3

使用该方式初始化的切片对象cap=len=初始化内容中的元素个数

② 依托已有数组初始化:

由于切片的底层就是数组,所以可以直接从数组中取部分或全部初始化切片:

切片名 := 数组名[起始索引:终止索引]
//例如:
sli := array[0:2333]     // 取array[0]~array[2332]组成切片

范围的选取原则可以参考字符串string取字串的操作,真实取到的范围是[起始索引,终止索引),前开后闭。同理省略起始索引表示从头开始、省略终止索引表示一直到结尾。

空切片(nil)

切片定义/声明但未初始化,默认为nil,长度为0。

参考以下代码:

package main

import "fmt"

func main() {
	var sli []int
	fmt.Println(sli)
	fmt.Printf("sli is nil? %t\n", sli == nil)
}

运行结果如下,确实为nil空切片,因此可以用s==nil来判断是否为空切片: image.png

追加(append)和复制(copy)

追加即添加新元素到切片中,使用内置函数append,标准语句如下:

原切片名 = append(原切片名,追加元素1,追加元素2,...,追加元素n)
//例如:
sli = append(sli, 1, 2, 3)

注意一定要用原来的切片指针接收append结果,因为append底层操作实际上是重新分配空间并将新位置返回给原指针。

复制即将原切片的所有元素复制到目标切片中,更新目标切片的长度len值,使用内置函数copy,标准语句如下:

copy(目标切片名, 源切片名)
//例如:
copy(slice_dest,slice_init)

参考以下示例代码:

package main

import "fmt"

func main() {
	sli := []int{1, 2, 3, 4, 5}  //初始长度为5,容量为5
	sli_des := make([]int, 6, 6) //目标切片长度和容量定义更小
	fmt.Println(sli)
	fmt.Println(sli_des)
	fmt.Printf("sli     length: %d  capacity: %d\n", len(sli), cap(sli))
	fmt.Printf("sli_des length: %d  capacity: %d\n", len(sli_des), cap(sli_des))
	copy(sli_des, sli)
	fmt.Println(sli_des)
	fmt.Printf("sli_des length: %d  capacity: %d\n", len(sli_des), cap(sli_des))
}

上述代码使用一个更大容量的切片来接收复制的切片内容,输出结果如下: image.png 但是如果使用一个容量更小的切片接收较大的切片,则会出发生截断,代码如下(仅将slice_des容量和长度改为4):

package main

import "fmt"

func main() {
	sli := []int{1, 2, 3, 4, 5}  //初始长度为5,容量为5
	sli_des := make([]int, 4, 4) //目标切片长度和容量定义更小
	fmt.Println(sli)
	fmt.Println(sli_des)
	fmt.Printf("sli     length: %d  capacity: %d\n", len(sli), cap(sli))
	fmt.Printf("sli_des length: %d  capacity: %d\n", len(sli_des), cap(sli_des))
	copy(sli_des, sli)
	fmt.Println(sli_des)
	fmt.Printf("sli_des length: %d  capacity: %d\n", len(sli_des), cap(sli_des))
}

结果如下,可见原来的[1,2,3,4,5]被截断为[1,2,3,4]: image.png

字典(map)

Go语言中的字典map和C++ STL标准库中的map是一样的,都是通过哈希实现的无序键值对集合。

map创建通常有两种方式:

  1. 内置make函数创建,标准语句如下:
字典名 := make(map[key]value)
//例如:
map0 := make(map[string]int)    //map from string to int
  1. 字面值语法创建方法,标准语句如下:
字典名 := map[key]value{
    key1 : value1
    key2 : value2
    ...
    keyn : valuen
}
//例如:
wheel := map[string]int{
    "car" : 4
    "bicycle" : 2
    "tricycle" : 3
    "human" : 0
} //我也不知道我怎么想到这个例子的

对应创建空map即为map[string]int{},空map为nil,不能用于存放键值对。

添加键值对需要先确保 map!=nil,再直接使用mapName[key]=value的形式添加即可。 删除键值对使用内置的delete函数,标准语句如下:

delete(key,value)

结构体

和数组/切片不同,结构体可以存放不同类型的数据,可以将同一个研究对象不同类型的性质数据归类到一起。

结构体定义需要使用typestruct语句,标准语法如下:

type 结构体名 struct{
    数据成员定义
}

定义后可以按照如下格式对结构体变量进行声明:

变量名 := 结构体名{ value1, value2,..., valuen}
变量名 := 结构体名{ key1:value1, key2:value2,..., key3:value3}

和C语言相同,Go语言的结构体可以使用.访问元素,同时可以定义结构体指针。

本篇主要介绍数据类型中的复合数据类型,包括数组、切片、字典、结构体。