golang的slice不仅有长度,还有容量的概念。在append的时候,slice如果扩容,内存会重新分配。频繁重新分配内存是有消耗的,通常情况下,可以直接使用make([]byte, 0, len(list))的方式提前分配内存。

但是一些特定的业务场景,需要对多个slice进行拼接,且数据源slice数量大小不确定,有的还会涉及并发。比如要获取30天的数据进行处理,开启了30个协程获取每一天的数据,最后append拼接成一个slice进行处理。

如果不提前分配内存,实现思路大致如下,为了并发安全,需要在append的时候使用锁

func getMonthRecord() []byte {
	var result []byte

	wg := sync.WaitGroup{}
	appendLock := sync.Mutex{}

	for i := 0; i < 30; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()
			
			currentList := getDayRecord()
			appendLock.Lock()
			result = append(result, currentList...)
			appendLock.Unlock()
		}()
	}

	wg.Wait()
	return result
}

如果要提前分配内存,代码如下。不仅不需要多次分配内存,而且固定长度的dayRecordList可以直接赋值,可以不需要使用锁。

func getMonthRecord() []byte {
	dayRecordList := make([][]byte, 30)

	wg := sync.WaitGroup{}

	for i := 0; i < 30; i++ {
		wg.Add(1)

		go func(ind int) {
			defer wg.Done()
			dayRecordList[ind] = getDayRecord()
		}(i)
	}

	wg.Wait()
	
	return aggregateRecord(dayRecordList)
}

// 分配内存并拼接slice
func aggregateRecord(dayRecordList [][]byte) []byte {
	var totalNum int
	for _, list := range dayRecordList {
		totalNum += len(list)
	}

	result := make([]byte, 0, totalNum)
	for _, list := range dayRecordList {
		result = append(result, list...)
	}

	return result
}

为了验证对性能的影响,简单实现getDayRecord如下,执行次数为1000。但是由于time.Sleep的影响,减去10s后,很尴尬地发现并没有什么差别,于是去掉time.Sleep。没有提前分配内存,为63ms左右。提前分配内存,为25ms左右

func getDayRecord() []byte {
	time.Sleep(10 * time.Millisecond)
	return make([]byte, 1000)
}

使用更加简单粗暴的方法进行验证。循环10000次的差距更加明显,不提前分配内存151ms,提前分配内存31ms,差了几乎4倍数

  dayList := make([][]byte, 0, 30)
	for i := 0; i < 30; i++ {
		dayList = append(dayList, getDayRecord())
	}

  // 不提前分配内存
	for i := 0; i < 10000; i++ {
		var result []byte
		for _, list := range dayList {
			result = append(result, list...)
		}
	}

  // 提前分配内存
	for i := 0; i < 10000; i++ {
		var totalNum int
		for _, list := range dayList {
			totalNum += len(list)
		}

		result := make([]byte, 0, totalNum)
		for _, list := range dayList {
			result = append(result, list...)
		}
	}
	

多个slice的拼接,计算完总长度后提前分配内存,虽然代码多了些,但是并不没有增加多少理解难度,在效率上也是更高的。还是建议使用提前分配内存的写法