复制 type Array struct {
len int64
elem Type
}
type Slice struct {
elem Type
}
// Type表示Go的类型
// 所有类型都实现Type接口。
type Type interface {
// Underlying 返回类型的基础类型
Underlying() Type
// String 返回类型的字符串表示形式
String() string
}
共同点:都属于集合类的类型,可以存储某一种类型的值;应用索引表达式得到值,应用切片表达式得到新切片
数组的长度必须在声明时给定,且不可变,它是数组类型的一部分 。
切片的类型字面量中,只有元素类型,长度随着元素数量增长而增长,但不会减小 。
切片可以看成是对数组的一层简单封装,在每个切片的底层数据结构中,一定会包含一个数组。
切片:是对数组的某个连续片段的引用,切片的容量代表了其可见底层数组的长度,切片的底层数组长度不可变
关于数组和切片,golang官方博客有文章详细说明,点击这里 。其实这里说的已经很清楚了,论好好阅读官方说明的重要性。
引用类型与值类型
引用类型:
切片类型(切片是对数组某个连续片段的引用)【相对于slice底层的数组而言,其实slice是一个结构体类型(也就是值类型)】
值类型:
切片类型【相对于slice底层的数组而言,其实slice是一个结构体类型(也就是值类型)】
Go语言中不存在“传值或传引用”的问题,在Go语言中只要看被传递的值的类型 ,如果被传递的值是引用类型,那就是“传引用”,如果被传递的值是值类型,那就是“传值”。从传递成本的角度,引用类型的值比值类型的值成本低很多。
复制 // 切片类型
type slice struct {
array unsafe.Pointer
len int
cap int
}
// 直接通过索引操作时,是对切片底层的数组进行操作,通过array指针实现
// 切片作为参数时,传递的是切片的副本
切片注意点
复制 package main
import (
"fmt"
)
func main() {
slice := []int{0, 1, 2, 3}
fmt.Printf("slice: %v slice addr %p \n", slice, &slice)
// slice: [0 1 2 3] slice addr 0xc00000c080
ret := changeSlice(slice)
fmt.Printf("slice: %v slice addr %p | ret: %v ret addr %p \n", slice, &slice, ret, &ret)
// slice: [0 111 2 3] slice addr 0xc00000c080 | ret: [0 111 2 3] ret addr 0xc00000c0c0
res := appendSlice(slice)
fmt.Printf("slice: %v slice addr %p | res: %v ret addr %p \n", slice, &slice, res, &res)
// slice: [0 111 2 3] slice addr 0xc00000c080 | res: [0 111 2 3 1] ret addr 0xc00000c120
}
// 因为,slice是一个结构体且参数传递是值传递,所以changeSlice()函数中的s是slice的一个副本,
// 所以changeSlice()函数的返回值ret的地址与slice不同,他们是内存中的两个对象。
func changeSlice(s []int) []int {
s[1] = 111
return s
}
// 在slice中array是一个指针,指向底层数组的开头,所以在changeSlice()函数中s[1] = 111是对底层数组的修改。
// 那么在main()函数中不论是读取slice还是读取ret,他们都指向同一个底层数组,所以看起来就是changeSlice()函数修改了传入的切片对象的原始值。
// 在appendSlice()函数中的append()操作是作用在res上而不是slice上。
func appendSlice(s []int) []int {
s = append(s, 1)
return s
}
从上面代码和输出结果(注释部分)可以看出:
changeSlice()
函数对外部slice生效了
Go中只有值传递 ,所有的引用传递 都是直接把对应的指针拷贝过去了,所以修改能直接在原对象生效。
很多地方都说slice是引用类型(这是相对于slice底层的数组而言的),其实slice是一个结构体类型(也就是值类型)。
因为slice其实是一个结构体而不是一个引用,要让appendSlice
生效,只要传入引用就可以,代码修改如下:
复制 res := appendSlice(&slice)
func appendSlice(s *[]int) *[]int {
*s = append(*s, 1)
return s
}
内建函数
数组容量永远和长度相等,且不可变 。
复制 // usage of make()
// 返回值而不是指针
make(types interface{},length int,capacity int)
// types:要创建的类型
// length:该类型的长度
// capacity:该类型的容量
len
官方标准库中的描述:
For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's "Length and capacity" section for details.
当参数是字符串字面量和简单 array 表达式,len 函数返回值是常量,这很重要。后半句更重要。
内置函数 len 和 cap 获取各种类型的实参并返回一个 int 类型结果。实现会保证结果总是一个 int 值。
如果 s 是一个字符串常量,那么 len(s)
是一个常量 。
如果 s 类型是一个数组或到数组的指针且表达式 s 不包含通道接收或(非常量的)函数调用的话,那么表达式 len(s)
和 cap(s)
是常量;这种情况下,s 是不求值的。否则的话, len 和 cap 的调用结果不是常量且 s 会被求值。
切片与底层数组
复制 // make函数或切片值字面量初始化时,切片最左边与底层数组最左边对应
slice1 := make([]int,3,5)
sliec2 := []int{1,2,3,4,5,6,7,8}
// 切片表达式创建切片时,切片最左边与底层数组索引对应,slice3的最左边对应slice2的索引3
slice3 := slice2[3:6]
// slice3的容量为可见底层数组的容量,即底层数组减去切片起始索引值
// slice3向右扩展至最大
slice3 = slice3[0:cap(slice3)]
切片表达式是一个开区间,得到的新切片的容量和长度为区间的差值。新切片的起始值为原切片或数组对应的索引值。切片无法向左扩展,但是可以向右扩展。
当切片无法容纳更多元素时,Go语言会进行扩容,不会改变原切片,而是创建一个容量更大的新切片 ,将原来的元素和新的元素一起拷贝到新的切片中。一般情况扩容为原来的2倍 ,当原切片的长度大于等于1024后,一次增长1.25倍 的方式逐渐扩容。
切片在扩容时,创建了新的切片和新的底层数组,原来的切片和底层数组没有任何改动。
在底层数组容量(即切片容量)足够的情况下append()
函数返回的是指向原底层数组 的切片
在底层数组容量(即切片容量)不够的情况下append()
函数返回的是指向新底层数组 的新切片
可寻址与不可寻址
复制 package main
import (
"crypto/sha1"
"fmt"
)
func main() {
input := []byte("Hello, playground")
hash := sha1.Sum(input)[:5] // 这里通不过编译
fmt.Println(hash)
}
// output
./test.go:8:28: invalid operation sha1.Sum(input)[:5] (slice of unaddressable value)
看一下crypto/sha1
库:
复制 // The size of a SHA-1 checksum in bytes.
const Size = 20
// The blocksize of SHA-1 in bytes.
const BlockSize = 64
func Sum(data []byte) [Size]byte {
var d digest
d.Reset()
d.Write(data)
return d.checkSum()
}
sha1.Sum()
返回一个长度是20的数组,而不是切片(如果是切片不会报错)。
Go是返回数值的,所以这里是20字节的数组,而不是指向它的指针。
不可寻址
大多数匿名值都不可寻址(复合字面值 是一个例外)。
在上面的代码中,sha1.Sum()
的返回值是匿名的,因为我们立即对其进行了切片操作。如果我们将它存在变量中,并因此使其变为非匿名,就是可寻址的,则该代码不会报错,如下所示。
复制 tmp := sha1.Sum(input)
hash := tmp[:5]
因为对数组进行切片操作要求该数组是可寻址的 ,sha1.Sum()
返回的匿名数组是不可寻址的,因此对其进行切片会被编译器拒绝。
如果在这里允许对不可寻址的匿名值进行切片操作,那么 Go 要默默地实现堆存储 以容纳 sha1.Sum()
的返回值(然后将该值复制到另一个值),该返回值将一直存在直到那个切片被回收。
注意:Go 语言规范中的许多内容要求或仅对可寻址的 值适用。例如,大多数赋值操作需要可寻址性。
方法调用
假设有一个类型 T,并且在 *T
上定义了一些方法,例如 *T.Op()
。就像 Go 允许在不取消引用指针的情况下进行字段引用一样,可以在非指针值上调用指针方法:
复制 type T struct{}
func (t *T)Op(){}
// afunc() 返回一个 T
func aFunc() T{ return T }
var x T
x.Op() // 这是 (&x).Op() 的简便写法
// 此简便写法需要获取地址,因此需要可寻址性,以下操作报错
aFunc().Op()
// 但是这个可以运行
var x T = aFunc()
x.Op()
并发访问切片
由于 slice或者map 是引用类型,golang 函数是传值调用,所用参数副本依然是原来的 slice或者map, 并发访问同一个资源会导致竞态条件。
复制 package main
import (
"fmt"
"sync"
)
func main() {
var (
slc []int
n = 1000
wg sync.WaitGroup
)
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
slc = append(slc, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println("len->", len(slc))
fmt.Println("cap->", cap(slc))
}
// output
len-> 997
cap-> 1024
真实的输出并没有达到我们的预期,len(slice) < n
。
slice是对数组一个连续片段的引用,当 slice 长度增加的时候,底层的数组会被换掉。当在换底层数组之前,切片同时被多个 goroutine 拿到,并执行 append 操作。那么很多 goroutine 的 append 结果会被覆盖 ,导致 n 个 gouroutine append 后,长度小于n。
go 1.9 增加sync.map
实现并发安全,slice咋整?
使用互斥锁
优点是比较简单,适合对性能要求不高的场景。
复制 package main
import (
"fmt"
"sync"
)
func main() {
var (
slc []int
n = 1000
wg sync.WaitGroup
lock sync.Mutex
)
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
lock.Lock()
defer lock.Unlock()
slc = append(slc, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println("len->", len(slc))
fmt.Println("cap->", cap(slc))
}
通道串行化
实现相对复杂,优点是性能很好,利用了channel的优势。
复制 package main
import (
"fmt"
"sync"
)
type Service struct {
ch chan int // 同步channel
data []int // 存储数据的slice
}
func (s *Service) Schedule() {
for i := range s.ch {
s.data = append(s.data, i)
}
}
func (s *Service) Close() {
close(s.ch)
}
func (s *Service) AddData(v int) {
s.ch <- v
}
func NewScheduler(size int, done func()) *Service {
s := &Service{
ch: make(chan int, size),
data: make([]int, 0),
}
go func() {
s.Schedule()
done()
}()
return s
}
func main() {
var (
n = 1000
wg sync.WaitGroup
)
c := make(chan struct{})
s := NewScheduler(n, func() { c <- struct{}{} })
wg.Add(n)
for i := 0; i < n; i++ {
go func(v int) {
defer wg.Done()
s.AddData(v)
}(i)
}
wg.Wait()
s.Close()
<-c
fmt.Println("len->", len(s.data))
fmt.Println("cap->", cap(s.data))
}