结构体
一个结构体(struct
)就是一组字段(field)。
package main
import "fmt"
type Vertex struct {X intY int
}
func main() {fmt.Println(Vertex{1, 2})
}
结构体中的字段用 . 访问
package main
import "fmt"
type Vertex struct {X intY int
}
func main() {v := Vertex{1, 2}v.X = 4fmt.Println(v.X)
}
结构体指针
结构体字段可以通过结构体指针来访问。
如果我们有一个指向结构体的指针 p
,那么可以通过 (*p).X
来访问其字段 X
。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X
就可以。
package main
import "fmt"
type Vertex struct {X intY int
}
func main() {v := Vertex{1, 2}p := &vp.X = 1e9fmt.Println(v)
}
结构体文法
结构体文法通过直接列出字段的值来新分配一个结构体。
使用 Name:
语法可以仅列出部分字段。(字段名的顺序无关。)
特殊的前缀 &
返回一个指向结构体的指针。
package main
import "fmt"
type Vertex struct {X, Y int
}
var (v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体v2 = Vertex{X: 1} // Y:0 被隐式地赋予v3 = Vertex{} // X:0 Y:0p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {fmt.Println(v1, p, v2, v3)
}
数组与切片
数组声明和初始化
数组是具有相同 唯一类型 的一组以编号且长度固定的数据项序列。
注意事项:
-
如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型,当使用值时我们必须先做一个类型判断。
-
数组长度最大为 2Gb
声明格式:
var identifier [len]type
Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)。
提出问题:这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2 的类型是 [5]int。
数组常量
如果数组值已经提前知道了,那么可以通过 数组常量 的方法来初始化数组,而不用依次使用 []=
方法(所有的组成元素都有相同的常量语法)。
package main
import "fmt"
func main() {// var arrAge = [5]int{18, 20, 15, 22, 16}// var arrLazy = [...]int{5, 6, 7, 8, 22}// var arrLazy = []int{5, 6, 7, 8, 22}var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}// var arrKeyValue = []string{3: "Chris", 4: "Ron"}
for i:=0; i < len(arrKeyValue); i++ {fmt.Printf("Person at %d is %s\n", i, arrKeyValue[i])}
}
多维数组
数组通常是一维的,但是可以用来组装成多维数组,例如:[3][5]int
,[2][2][2]float64
。
内部数组总是长度相同的。Go 语言的多维数组是矩形式的
将数组传递给函数
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:
-
传递数组的指针
-
使用数组的切片
package main
import "fmt"
func main() {array := [3]float64{7.0, 8.5, 9.1}x := Sum(&array) // Note the explicit address-of operator// to pass a pointer to the arrayfmt.Printf("The sum of the array is: %f", x)
}
func Sum(a *[3]float64) (sum float64) {for _, v := range *a { // derefencing *a to get back to the array is not necessary!sum += v}return
}
切片
概念
-
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。
-
切片下界的默认值为
0
,上界则是该切片的长度。 -
切片是可索引的,并且可以由 len() 函数获取长度。
-
和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组。
-
切片提供了计算容量的函数
cap()
可以测量切片最长可以达到多少:它等于切片从第一个元素开始,到相关数组末尾的元素个数。 -
切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:
0 <= len(s) <= cap(s)
。 -
多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块
-
切片
s
的长度和容量可通过表达式len(s)
和cap(s)
来获取。 -
切片的零值是
nil
。nil 切片的长度和容量为 0 且没有底层数组。
package main
import "fmt"
func main() {q := []int{2, 3, 5, 7, 11, 13}fmt.Println(q)
r := []bool{true, false, true, true, false, true}fmt.Println(r)
s := []struct {i intb bool}{{2, true},{3, false},{5, true},{7, true},{11, false},{13, true},}fmt.Println(s)
}
声明切片的格式是: var identifier []type
(不需要说明长度)。
一个切片在未初始化之前默认为 nil,长度为 0。
切片的初始化格式是:var slice1 []type = arr1[start:end]
。(这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0] 就等于 arr1[start]。)
注意:
-
如果某个人写:var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1。
-
如果 s2 是一个 slice,你可以将 s2 向后移动一位 s2 = s2[1:],但是末尾没有移动。切片只能向后移动,s2 = s2[-1:] 会导致编译错误。切片不能被重新分片以获取数组的前一个元素。
用 make () 创建一个切片
当相关数组还没有定义时,我们可以使用 make () 函数来创建一个切片 同时创建好相关数组:var slice1 []type = make([]type, len)
。也可以简写为 slice1 := make([]type, len)
,这里 len
是数组的长度并且也是 slice
的初始长度。
new () 和 make () 的区别
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
-
new (T) 为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 * T 的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
-
make(T) 返回一个类型为 T 的初始值,它只适用于 3 种内建的引用类型:切片、map 和 channel
多维 切片
和数组一样,切片通常也是一维的,但是也可以由一维组合成高维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配(通过 make 函数)。
bytes 包
类型 []byte 的切片十分常见,Go 语言有一个 bytes 包专门用来解决这种类型的操作方法。
bytes 包和字符串包十分类似(参见第 4.7 节)。而且它还包含一个十分有用的类型 Buffer:
import "bytes"
type Buffer struct {...
}
Buffer 可以这样定义:
var buffer bytes.Buffer
或者使用 new 获得一个指针:
var r *bytes.Buffer = new(bytes.Buffer)
或者通过函数:func NewBuffer(buf []byte) *Buffer,创建一个 Buffer 对象并且用 buf 初始化好;NewBuffer 最好用在从 buf 读取的时候使用。
通过 buffer 串联字符串
类似于 Java 的 StringBuilder 类。
var buffer bytes.Buffer
for {if s, ok := getNextString(); ok { //method getNextString() not shown herebuffer.WriteString(s)} else {break}
}
fmt.Print(buffer.String(), "\n")
切片重组
我们已经知道切片创建的时候通常比相关数组小,例如:
slice1 := make([]type, start_length, capacity)
其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。
这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下:
slice1 = slice1[0:end]
,其中 end 是新的末尾索引(即长度)。
切片的复制与追加
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
package main
import "fmt"
func main() {sl_from := []int{1, 2, 3}sl_to := make([]int, 10)
n := copy(sl_to, sl_from)fmt.Println(sl_to)fmt.Printf("Copied %d elements\n", n) // n == 3
sl3 := []int{1, 2, 3}sl3 = append(sl3, 4, 5, 6)fmt.Println(sl3)
}
如果使用append函数超过了切片的容量会怎样?
答:
-
当使用
append
函数向切片添加元素时,如果超过了切片的容量,切片将会重新分配更大的内存空间,并将原来的元素复制到新的内存空间中。这意味着切片的长度和容量会增加,并且原来的元素会被复制到新的内存位置上。 -
具体来说,如果切片的容量不足以容纳新的元素,则
append
函数会创建一个新的底层数组,并将原来的元素复制到这个新数组中。然后,新的元素会被添加到新的底层数组中,并返回一个指向新数组的切片。 -
这也意味着如果你有一个指向原切片的指针,在调用
append
后,原切片可能会指向一个不同的内存地址,因为底层数组已经改变了。
学习参考资料:
《Go 入门指南》 | Go 技术论坛 (learnku.com)
Go 语言之旅