【Go】--make函数和append函数
Go 中的 make 函数和 append 函数
make 函数的作用
make
函数专门用于创建和初始化 slice、map 和 channel 这三种内建引用类型- 对于 slice,可以指定长度和容量;对于 map,可以指定初始容量;对于 channel,可以指定缓冲区大小
make
返回的是已初始化的类型引用,可以直接使用- 合理使用预分配容量可以显著提高程序性能,特别是在处理大量数据时
- 与
new
函数不同,make
会执行完整的初始化过程,而不仅仅是内存分配
append 函数的作用
append
函数用于向切片(slice)添加元素- 可以添加单个元素或多个元素
- 可以合并两个切片
- 自动处理切片的扩容机制
- 是操作切片最常用的函数之一
1. make 函数的基本概念
make
是 Go 语言中的一个内置函数,主要用于创建并初始化以下三种内建的引用类型:
- slice(切片)
- map(映射)
- channel(通道)
与 new
函数不同,make
不仅分配内存,还会进行初始化操作,返回的是类型的引用(而不是指针)。
2. make 函数的语法格式
make(T, args...)
其中:
T
:要创建的类型(slice、map 或 channel)args...
:根据类型不同而变化的参数
3. 用于不同数据类型的详细用法
3.1 使用 make 创建切片(slice)
语法:
make([]T, length, capacity)
参数说明:
T
:切片元素的类型length
:切片的长度(当前包含的元素个数)capacity
:切片的容量(可选参数,默认等于 length)
示例:
package mainimport "fmt"func main() {// 创建长度为 3,容量为 5 的整型切片slice1 := make([]int, 3, 5)fmt.Printf("slice1: %v, len: %d, cap: %d\n", slice1, len(slice1), cap(slice1))// 输出: slice1: [0 0 0], len: 3, cap: 5// 创建长度和容量都为 3 的字符串切片slice2 := make([]string, 3)fmt.Printf("slice2: %v, len: %d, cap: %d\n", slice2, len(slice2), cap(slice2))// 输出: slice2: [ ], len: 3, cap: 3// 不指定容量,容量默认等于长度slice3 := make([]int, 3)fmt.Printf("slice3: %v, len: %d, cap: %d\n", slice3, len(slice3), cap(slice3))// 输出: slice3: [0 0 0], len: 3, cap: 3
}
注意事项:
- 使用
make
创建的切片会被自动初始化为元素类型的零值 - 容量必须大于等于长度
3.2 使用 make 创建映射(map)
语法:
make(map[K]V, initialCapacity)
参数说明:
K
:键的类型V
:值的类型initialCapacity
:初始容量(可选参数)
示例:
package mainimport "fmt"func main() {// 创建字符串到整型的映射,不指定初始容量map1 := make(map[string]int)map1["apple"] = 5map1["banana"] = 3fmt.Printf("map1: %v\n", map1)// 输出: map1: map[apple:5 banana:3]// 创建字符串到字符串的映射,指定初始容量为 10map2 := make(map[string]string, 10)map2["name"] = "Alice"map2["city"] = "Beijing"fmt.Printf("map2: %v\n", map2)// 输出: map2: map[city:Beijing name:Alice]fmt.Printf("map1 len: %d\n", len(map1))fmt.Printf("map2 len: %d\n", len(map2))
}
注意事项:
- 初始容量只是提示性的,映射会根据需要自动扩容
- 使用
make
创建的映射是空的,可以立即进行键值对操作
3.3 使用 make 创建通道(channel)
语法:
make(chan T, bufferSize)
参数说明:
T
:通道传输的数据类型bufferSize
:缓冲区大小(可选参数)
示例:
package mainimport ("fmt""time"
)func main() {// 创建无缓冲的整型通道ch1 := make(chan int)// 创建缓冲大小为 3 的字符串通道ch2 := make(chan string, 3)// 使用无缓冲通道的示例go func() {time.Sleep(1 * time.Second)ch1 <- 42 // 发送数据}()// 使用有缓冲通道的示例ch2 <- "hello"ch2 <- "world"ch2 <- "golang"fmt.Printf("ch2 len: %d, cap: %d\n", len(ch2), cap(ch2))// 输出: ch2 len: 3, cap: 3// 从无缓冲通道接收数据value := <-ch1fmt.Printf("Received from ch1: %d\n", value)// 输出: Received from ch1: 42// 从有缓冲通道接收数据fmt.Printf("Received from ch2: %s\n", <-ch2)fmt.Printf("Received from ch2: %s\n", <-ch2)fmt.Printf("Received from ch2: %s\n", <-ch2)
}
注意事项:
- 无缓冲通道(bufferSize=0 或省略)是同步的,发送和接收操作会阻塞直到另一端准备好
- 有缓冲通道是异步的,只有在缓冲区满时发送才会阻塞,缓冲区空时接收才会阻塞
4. make 与 new 的区别
特性 | make | new |
---|---|---|
适用类型 | slice、map、channel | 任意类型 |
返回值 | 类型的引用(T) | 类型的指针(*T) |
初始化 | 会进行初始化 | 只分配零值内存 |
零值 | 返回已初始化的值 | 返回指向零值的指针 |
示例对比:
package mainimport "fmt"func main() {// 使用 make 创建切片slice1 := make([]int, 3)fmt.Printf("make slice: %v, type: %T\n", slice1, slice1)// 输出: make slice: [0 0 0], type: []int// 使用 new 创建切片slice2 := new([]int)fmt.Printf("new slice: %v, type: %T\n", slice2, slice2)// 输出: new slice: &[], type: *[]int// 使用 make 创建映射map1 := make(map[string]int)map1["key"] = 1fmt.Printf("make map: %v, type: %T\n", map1, map1)// 输出: make map: map[key:1], type: map[string]int// 使用 new 创建映射map2 := new(map[string]int)// (*map2)["key"] = 1 // 这会导致 panic,因为映射未初始化fmt.Printf("new map: %v, type: %T\n", map2, map2)// 输出: new map: &map[], type: *map[string]int
}
6. append 函数
6.1 append 函数的基本概念
append
是 Go 语言中的一个内置函数,专门用于向切片(slice)添加元素。它是操作切片最常用的函数之一。
6.2 append 函数的语法格式
append(slice []T, elements ...T) []T
参数说明:
slice
:目标切片elements
:要添加的元素(可以是单个元素或多个元素)- 返回值:新的切片(可能指向新的底层数组)
6.3 append 函数的使用方法
6.3.1 添加单个元素
package mainimport "fmt"func main() {// 创建空切片slice := make([]int, 0, 5)// 添加单个元素slice = append(slice, 1)slice = append(slice, 2)slice = append(slice, 3)fmt.Printf("切片内容: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))// 输出: 切片内容: [1 2 3], 长度: 3, 容量: 5
}
6.3.2 添加多个元素
package mainimport "fmt"func main() {slice := []int{1, 2, 3}// 添加多个元素slice = append(slice, 4, 5, 6)fmt.Printf("切片内容: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))// 输出: 切片内容: [1 2 3 4 5 6], 长度: 6, 容量: 6
}
6.3.3 合并两个切片
package mainimport "fmt"func main() {slice1 := []int{1, 2, 3}slice2 := []int{4, 5, 6}// 合并两个切片(注意使用 ... 展开操作符)slice1 = append(slice1, slice2...)fmt.Printf("合并后的切片: %v\n", slice1)// 输出: 合并后的切片: [1 2 3 4 5 6]
}
6.3.4 在指定位置插入元素
package mainimport "fmt"func main() {slice := []int{1, 2, 4, 5}// 在索引2的位置插入元素3index := 2element := 3// 方法:使用 append 和切片的组合slice = append(slice[:index], append([]int{element}, slice[index:]...)...)fmt.Printf("插入后的切片: %v\n", slice)// 输出: 插入后的切片: [1 2 3 4 5]// 更简洁的插入方法slice2 := []int{1, 2, 4, 5}slice2 = append(slice2, 0) // 先扩展切片copy(slice2[index+1:], slice2[index:]) // 移动元素slice2[index] = element // 插入新元素fmt.Printf("插入后的切片2: %v\n", slice2)// 输出: 插入后的切片2: [1 2 3 4 5]
}
6.4 append 函数的扩容机制
package mainimport "fmt"func main() {// 演示 append 的扩容过程slice := make([]int, 0, 2)fmt.Printf("初始状态 - 长度: %d, 容量: %d\n", len(slice), cap(slice))for i := 1; i <= 10; i++ {slice = append(slice, i)fmt.Printf("添加 %d - 长度: %d, 容量: %d\n", i, len(slice), cap(slice))}/* 输出示例:初始状态 - 长度: 0, 容量: 2添加 1 - 长度: 1, 容量: 2添加 2 - 长度: 2, 容量: 2添加 3 - 长度: 3, 容量: 4 (第一次扩容)添加 4 - 长度: 4, 容量: 4添加 5 - 长度: 5, 容量: 8 (第二次扩容)添加 6 - 长度: 6, 容量: 8添加 7 - 长度: 7, 容量: 8添加 8 - 长度: 8, 容量: 8添加 9 - 长度: 9, 容量: 16 (第三次扩容)添加 10 - 长度: 10, 容量: 16*/
}
6.5 append 函数的注意事项
6.5.1 返回值必须接收
package mainimport "fmt"func main() {slice := []int{1, 2, 3}// 错误:append 的返回值必须接收// append(slice, 4) // 这样不会修改原切片// 正确:接收返回值slice = append(slice, 4)fmt.Printf("正确使用: %v\n", slice)// 输出: 正确使用: [1 2 3 4]
}
6.5.2 切片共享问题
package mainimport "fmt"func main() {// 创建底层数组arr := [5]int{1, 2, 3, 4, 5}// 创建两个共享底层数组的切片slice1 := arr[1:4] // [2, 3, 4]slice2 := slice1[0:2] // [2, 3]fmt.Printf("修改前 - slice1: %v, slice2: %v, arr: %v\n", slice1, slice2, arr)// 修改 slice2slice2[0] = 200fmt.Printf("修改 slice2 后 - slice1: %v, slice2: %v, arr: %v\n", slice1, slice2, arr)// 对 slice1 进行 append(可能触发扩容)slice1 = append(slice1, 6)slice1[0] = 300fmt.Printf("append slice1 后 - slice1: %v, slice2: %v, arr: %v\n", slice1, slice2, arr)
}
6.5.3 性能优化建议
package mainimport "fmt"func main() {// 性能优化:预分配容量// 方法1:使用 make 预分配容量efficientSlice := make([]int, 0, 1000)for i := 0; i < 1000; i++ {efficientSlice = append(efficientSlice, i)}// 方法2:一次性添加多个元素var batchSlice []intfor i := 0; i < 1000; i += 100 {batch := make([]int, 100)for j := 0; j < 100; j++ {batch[j] = i + j}batchSlice = append(batchSlice, batch...)}fmt.Printf("预分配容量 - 长度: %d, 容量: %d\n", len(efficientSlice), cap(efficientSlice))fmt.Printf("批量添加 - 长度: %d, 容量: %d\n", len(batchSlice), cap(batchSlice))
}
7. make 和 append 的配合使用
7.1 最佳实践示例
package mainimport "fmt"func main() {// 场景:处理大量数据时,先预分配容量再使用 append// 1. 使用 make 预分配容量expectedSize := 10000dataSlice := make([]int, 0, expectedSize)// 2. 使用 append 添加数据for i := 0; i < expectedSize; i++ {dataSlice = append(dataSlice, i*2)}// 3. 处理完成后可以截断不需要的部分if len(dataSlice) > 5000 {dataSlice = dataSlice[:5000]}fmt.Printf("最终结果 - 长度: %d, 容量: %d\n", len(dataSlice), cap(dataSlice))// 4. 如果需要释放内存,可以重新分配if cap(dataSlice) > len(dataSlice)*2 {// 创建新的切片,只保留需要的容量optimizedSlice := make([]int, len(dataSlice))copy(optimizedSlice, dataSlice)dataSlice = optimizedSlice}fmt.Printf("优化后 - 长度: %d, 容量: %d\n", len(dataSlice), cap(dataSlice))
}
7.2 实际应用场景
package mainimport ("fmt""strings"
)func processStrings(words []string) []string {// 预分配容量result := make([]string, 0, len(words))for _, word := range words {// 过滤和处理字符串if len(word) > 0 && !strings.Contains(word, "test") {processed := strings.ToUpper(word)result = append(result, processed)}}return result
}func main() {input := []string{"hello", "world", "test", "go", "programming"}output := processStrings(input)fmt.Printf("输入: %v\n", input)fmt.Printf("输出: %v\n", output)// 输出: 输入: [hello world test go programming]// 输出: 输出: [HELLO WORLD GO PROGRAMMING]
}
8. 常见使用场景和最佳实践
8.1 切片的最佳实践
package mainimport "fmt"func main() {// 当你知道大概需要多少元素时,预分配容量可以提高性能expectedSize := 1000// 好的做法:预分配容量efficientSlice := make([]int, 0, expectedSize)for i := 0; i < expectedSize; i++ {efficientSlice = append(efficientSlice, i)}// 不好的做法:让切片频繁扩容var inefficientSlice []intfor i := 0; i < expectedSize; i++ {inefficientSlice = append(inefficientSlice, i)}fmt.Printf("Efficient slice len: %d, cap: %d\n", len(efficientSlice), cap(efficientSlice))fmt.Printf("Inefficient slice len: %d, cap: %d\n", len(inefficientSlice), cap(inefficientSlice))
}
8.2 映射的最佳实践
package mainimport "fmt"func main() {// 当你知道大概需要存储多少键值对时,预分配容量可以减少重新哈希的次数expectedItems := 1000// 好的做法:预分配容量efficientMap := make(map[int]string, expectedItems)for i := 0; i < expectedItems; i++ {efficientMap[i] = fmt.Sprintf("value%d", i)}// 不好的做法:让映射频繁扩容inefficientMap := make(map[int]string)for i := 0; i < expectedItems; i++ {inefficientMap[i] = fmt.Sprintf("value%d", i)}fmt.Printf("Efficient map size: %d\n", len(efficientMap))fmt.Printf("Inefficient map size: %d\n", len(inefficientMap))
}