golang学习随便记3
Posted sjg20010414
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang学习随便记3相关的知识,希望对你有一定的参考价值。
复合数据类型(构造类型)
数组
和C语言一样,golang数组是固定长度的,索引方式也一样,不同的是,golang数组元素默认就是初始化的(为该类型的0值)。遍历方式略有不同。
golang数组也可以和C一样初始化来确定长度。
package main
import (
"fmt"
)
func main() {
var a [3]int
fmt.Println(a[0]) // 0
fmt.Println(a[len(a)-1]) // 0
for i, v := range a {
fmt.Printf("%d %d\\n", i, v) // 0 0 // 1 0 // 2 0
}
q := [...]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Printf("%T\\n", q) // [3]int
fmt.Println(r[2]) // 0
}
上面的代码中,我们知道,数组是“一类类型”,[3]int 和 [4]int 是两个不同的类型。
数组用初始化确定长度时,也可以是“乱序”的: r := [...]int { 99: -1} 确定一个长度为100的数组,前99个0,最后一个-1. 下面是结合枚举乱序初始化的
package main
import (
"fmt"
)
func main() {
type Currency int
const (
USD Currency = iota
EUR
GBP
RMB
)
symbol := [...]string{EUR: "€", RMB: "¥", USD: "$", GBP: "£"}
for i, v := range symbol {
fmt.Printf("%d %s\\n", i, v)
}
}
输出
0 $
1 €
2 £
3 ¥
和C语言不同,golang的数组是可以用 == 和 != 直接进行比较的,前提是两数组的对应的类型是可以比较的,例如两个长度为2的int数组,但[2]int 和[3]int 因为类型不同,无法比较。
和C语言不同,当把数组作为函数的参数时,golang是拷贝数组方式的值传递,golang对参数都是值传递!要避免低效的数组值传递,必须传递一个数组的指针给函数。
package main
import (
"fmt"
)
func main() {
var buf1, buf2 [32]byte
for i := 0; i < 32; i++ {
buf1[i] = 0x41
buf2[i] = 0x42
}
for _, v := range buf1 {
fmt.Print(v)
}
fmt.Println()
for _, v := range buf2 {
fmt.Print(v)
}
fmt.Println()
zero1(&buf1)
for _, v := range buf1 {
fmt.Print(v)
}
fmt.Println()
zero2(&buf2)
for _, v := range buf2 {
fmt.Print(v)
}
fmt.Println()
}
func zero1(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
func zero2(ptr *[32]byte) {
*ptr = [32]byte{}
}
输出
6565656565656565656565656565656565656565656565656565656565656565
6666666666666666666666666666666666666666666666666666666666666666
00000000000000000000000000000000
00000000000000000000000000000000
上面的代码中,我们可以了解到golang数组不同于C数组的一些特点:golang数组必须带上长度看才是一个类型,数组是可以被整体赋值覆盖的(C数组只能整体赋值初始化),例如 zero2()函数内,通过生成一个空数组覆盖ptr指向的数组的内容。
slice(切片)
slice总是有一个底层数组作为容器的,然后,slice有它的指针、容量cap(s)和长度len(s),指针指向slice可以访问的底层数组中的第一个元素,容量是指针位置到底层数组最后一个元素之间的元素个数,显然长度不能超过容量,但在容量范围内可以扩展当前的slice。某种程度上,slice具有STL vector的一些特点。slice是“动态数组”,多个slice可以共用同一个底层数组,这也是slice存在的意义。
将一个slice传递给函数时,函数内部可以修改底层数组的元素,因为slice是指针或叫别名引用。
package main
import (
"fmt"
)
func main() {
a := [...]int{0, 1, 2, 3, 4, 5}
reverse(a[:])
fmt.Println(a)
}
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
输出 [5 4 3 2 1 0]
上面的reverse函数中,参数s的类型是[]int,即不定长的“数组”,所以,它是一个切片,因为切片是不定长的。另外,如果用a作为reverse的参数,是不可能倒序a数组的,因为数组是值拷贝的,而用a[:]生成一个指向数组头部一直到尾部的切片作为参数,就可以,因为切片是指针(别名)。
slice是不定长的,不能用 == 或 != 来比较,除了字节slice标准库中有bytes.Equal可以实现比较,其它slice只能自己写函数实现比较。slice唯一可以用 == 比较的是 nil, nil 表示slice没有对应的底层数组(同时长度和容量当然为0),但 nil 和长度和容量为0指向空数组的切片不同,所以
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
如果不想显式创建底层数组而创建slice,可以使用内置函数make(创建匿名底层数组再创建切片):
make([]T, len)
make([]T, len, cap) // 作用和 make([]T, cap)[:len] 相同
再来看一个给slice追加元素的例子,使用和STL vector的底层一样的操作(内置的append函数的增长策略还要更复杂)
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Printf("cap = %d, len = %d\\n", cap(s), len(s))
ss := appendInt(s, 99)
fmt.Printf("cap = %d, len = %d\\n", cap(ss), len(ss))
var x, y []int
for i := 0; i < 10; i++ {
y = appendInt(x, i)
fmt.Printf("%d cap=%d\\t%v\\n", i, cap(y), y)
x = y
}
}
func appendInt(x []int, y int) []int {
var z []int // 使用新slice,因为尽管 slice 是不定长的,
zlen := len(x) + 1 // 但原来底层数组可能不足以再容纳额外元素
if zlen <= cap(x) { // slice x 还有剩余容量,z 直接切片 x 即可
z = x[:zlen]
} else { // slice x 已经用完容量
zcap := zlen // slice z 至少需要容纳添加上元素后的容量
if zcap < 2*len(x) { // z 的容量 < 2 倍 x的长度
zcap = 2 * len(x) // 直接将 z 容量扩展至 2倍 x的长度
}
z = make([]int, zlen, zcap) // 创建 slice z
copy(z, x) // 将 x 原来内容拷贝到 z
}
z[len(x)] = y // 添加要追加的元素
return z
}
输出
cap = 5, len = 5
cap = 10, len = 6
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
在上面的代码中,需要注意一点,ss := appendInt(s, 99),我们并不清楚(是不应该搞清楚)切片ss 和 切片s 是否使用不同的底层数组(也不能确定是否使用了相同的底层数组),而且,函数 appendInt 内部的 切片 z,如果它是新分配的内存,那么它的生命周期其实是“逃逸”到函数外的,从而 z 是分配在堆上的——golang的变量生命周期,不能按C/C++那样理解,因为它有GC,完全用变量是否还被访问来决定生命周期(包括分配在栈上还是堆上)。
所以,更合理的使用方式,应该把代码写成如下(“覆盖”s的写法,不管s底层是否重新分配了)
//............
s = appendInt(s, 99)
fmt.Printf("cap = %d, len = %d\\n", cap(s), len(s))
//............
内置的append函数可以接受不定参数个数的参数,如 (x... 为将 x打散)
var x []int
x = append(x, 1) // [1]
x = append(x, 2, 3) // [1 2 3]
x = append(x, 4, 5, 6) // [1 2 3 4 5 6]
x = append(x, x...) // [1 2 3 4 5 6 1 2 3 4 5 6]
fmt.Println(x)
我们自己也可以将 appendInt() 函数改造成可以接受可变长度参数列表的 (同时表明,内置函数copy 是支持变长的参数列表的)。 y ...int 表示 y 是一个变长的 int 列表,不再是确定的1个整数
func appendInt(x []int, y ...int) []int {
var z []int // 使用新slice,因为尽管 slice 是不定长的,
zlen := len(x) + len(y) // 但原来底层数组可能不足以再容纳额外元素
//..................
copy(z[len(x):], y) // 添加要追加的元素
return z
}
slice 的 就地修改,更能体现 slice 复用底层数组的特点。下面程序是从多个字符串中去掉空串
package main
import (
"fmt"
)
func main() {
data := []string{"one", "", "three"}
fmt.Printf("%q\\n", nonempty(data)) // ["one" "three"]
fmt.Printf("%q\\n", data) // ["one" "three" "three"]
}
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}
因为 slice 复用底层数组,可以就地修改,所以,对函数使用者来说,不能“信任”传入的slice data在调用完函数后内容是否保持不变,从而更合理的做法是直接将其覆盖,即 data = nonempty(data)
利用内置append函数和就地修改特性,还可以简化该函数
func nonempty(strings []string) []string {
out := strings[:0] // 引用原始 slice 且长度为0 的新的slice
for _, s := range strings {
if s != "" {
out = append(out, s)
}
}
return out
}
这个过滤的例子,理解上可以简单理解为 一堆 *string 指针,去掉了指向空串的指针,返回剩下的,而原来这些指针指向的字符串,大部分没动,只有就地修改部分的变动。
可以用 slice 实现栈,道理和STL vector实现栈一样
stack := []int{}
stack = append(stack, 3)
stack = append(stack, 5)
stack = append(stack, 8)
top := stack[len(stack)-1]
fmt.Println(top) // 8
stack = stack[:len(stack)-1]
fmt.Println(stack) // [3 5]
从 slice 中间移除一个元素(保持顺序)代价比较高,因为需要整体把后面的每个拷贝到前一个,内置函数copy(copy只要求目标序列不小于来源序列)可以简化代码。不保持顺序显然比较容易,把最后一个拷贝到移除位置,返回0到len(s)-1的切片
以上是关于golang学习随便记3的主要内容,如果未能解决你的问题,请参考以下文章