实践: go的slice到底是个啥 | 青训营

55 阅读4分钟

这几天在用GORM的时候碰到了海量的问题. GORM的官方文档很详细, 但是没啥目录索引, 几乎全部基于示例, 让读者会有一种阅读等于逆向工程的感觉. 遇到的不少问题只能自己实践了. 其中几个又常见又低级(非贬义)又令人强迫症大犯的是:

  1. 当函数返回结构体或者结构体(构成的)列表时, 到底有没有复制发生.
  2. GORM风格的增删改查中, 经常会用到xx.Find(&user) yy.Find(&users). 官方的示例很离谱, 也不知道这user/users到底是个啥, 在哪里定义的, 上边好远的框里定义的那个是不是就是这个框中的user/users... 所以, 用xx.Find(user)行否? 不传地址也会修改user原值吗? 传了地址/指针但没为其分配内存空间, GORM会自动分配吗?
  3. 这个slice怪的很. python的list太好用, 写惯python后其他语言啥都不会也许是真的. go的slice显然和python神中神的list不是一种东西: slice增添元素为啥要用append()返回新值? slice空间到底是怎么增长的, for + append()会不会慢到死?

首先来看1. 这个比较简单. go中函数在返回结构体时会产生复制, 而返回结构体列表时不会. 所以在封装GORM操作的时候可以放心大胆地:

func ReadVideoFavorited(ctx context.Context, id uint) (users []model.User, err error) {
xxx
err = DB.Model(&model.Video{Model: gorm.Model{ID: id}}).Select("id").Association("Favorited").Find(&users)
yyy
}

如此种种.

而在返回结构体时就尽量:

func ReadVideoBasics(ctx context.Context, id uint) (video *model.Video, err error) {
xxx
video = &model.Video{}
err = DB.Model(&model.Video{}).Where("id=?", id).First(video).Error
yyy
}

传结构体指针啦!

再来看2. 显然user之流是结构体, users之流是列表. 尚没有深入研究GORM代码, 只是做了些实验, 所以像上面代码一样传结构体指针的话, 先为其分配内存空间比较好, 不要传递指针的指针然后等着GORM修改外部指针的指向, 这样即便GORM不自动分配内存也不会越界或panic. 对于列表(slice), 问题就大了. 经过实验发现, 空slice被以地址形式传给GORM的话, 是可以正常工作的, 那么不传地址而是直接传递它本身会不会正常工作呢?

其实并不需要作死去尝试. 经过对3.的研究, 答案其实是不会!!! 看上去赋值slice不会产生复制, 但是这其实指的是它的底层数组不会复制. 用于指示起始和结束的indices等部分还是会被复制的, 将其直接在函数间传递, 虽然其看不到的底层数组的值相同+地址相同, 但其余部分不同, slice对象本身的地址也不同!!! 那么什么时候底层数组也会不同了呢? 聪明的你可能想到了, 既然是数组, 不是链表, 在不可避免的时刻对数组进行扩容时会产生复制+将新数组连接到slice对象上. 此时底层数组被更换了, 新旧两个slice就彻底没有共同之处了. 那么扩容常见于什么时候呢? 对了, cap不足时的append().

像上边GORM的例子, GORM若是要对非地址形式传入的空slice进行append的话, 显然它的底层数组变化(被新建)了. 也就是说, GORM函数内看到并处理的slice与外边的slice彻底脱离了关系, 此时内部做出的改变不会影响到外部, 外边的slice并没有被做出任何改变.

也许GORM内部有办法处理以上情况从而非地址传入列表也可用, 也许注重性能的它不会. 但是无论是GORM传slice还是自己的func传slice, 都要注意些为好.

现在slice对象的逻辑和结构似乎可以被推理出来了. 事先声明, 没有深入研究go内部源码. 不过想想看, 下边这种结构, 能作为slice使用吗? 它在赋值时的行为是怎样的?

type customSlice struct{
    start int
    end   int
    data  *actual_data
}
...
func (slc *customSlice) append(xx)(yy){
zzz
}
...