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的切片

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值