一.控制内存分配的速度
有效的方法就是控制内存分配的速度.过多的内存分配会导致频繁的垃圾回收,增加CPU的负载.
1.限制Goroutine的数量: 因为每个协程都需要分配空间栈,在GO语言中sync.WaitGroup机制控制协程的数量
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 确保任务完成时减少计数
fmt.Printf("协程 %d 开始工作\n", id)
time.Sleep(2 * time.Second) // 模拟耗时任务
fmt.Printf("协程 %d 完成工作\n", id)
}
func main() {
var wg sync.WaitGroup
// 假设有 5 个任务
numWorkers := 5
for i := 1; i <= numWorkers; i++ {
wg.Add(1) // 增加 WaitGroup 的计数
go worker(i, &wg) // 启动协程执行任务
}
// 等待所有协程完成
wg.Wait()
fmt.Println("所有任务完成")
}
二.字符串连接优化
在go语言中字符串连接(如使用 + 运算符)操作会频繁导致内存分配和拷贝,特别在循环中.
尽量少使用字符串连接,可以使用strings.Builder 或者bytes.Buffer类型来高效构建字符串.
字符串连接内存消耗大的原因:Go 中,字符串是不可变的(immutable),这意味着每次对字符串进行拼接时,都会创建一个新的字符串,并且将原字符串和新的内容复制到新的内存空间中。这会导致频繁的内存分配和复制操作,特别是在拼接大量字符串时,会产生较大的性能开销.使用 +
拼接多个字符串,每个拼接操作都会产生一个中间结果.
strings.Builder 或者bytes.Buffer消耗小的原因:strings.Builder
和 bytes.Buffer
在内部使用一个动态扩展的内存缓冲区,数据追加时不会立即创建新对象,而是直接修改内部缓冲区。当拼接完成时,内部缓冲区的数据会一次性转换为字符串。这样可以避免在拼接过程中多次复制数据。所有的拼接操作都在缓冲区内进行,避免了中间结果的生成,从而减少了内存使用
bytes.Buffer
处理的是 []byte
类型的字节切片,而 strings.Builder
处理的是字符串。
//使用 strings.Builder 示例
package main
import (
"fmt"
"strings"
)
func main() {
// 使用 strings.Builder 高效拼接字符串
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(" ")
builder.WriteString("World")
// 获取最终字符串
result := builder.String()
fmt.Println(result) // 输出: Hello World
}
//使用 bytes.Builder 示例
package main
import (
"bytes"
"fmt"
)
func main() {
// 使用 bytes.Buffer 高效拼接字符串
var buffer bytes.Buffer
buffer.WriteString("Hello")
buffer.WriteString(" ")
buffer.WriteString("World")
// 获取最终字符串
result := buffer.String()
fmt.Println(result) // 输出: Hello World
}
三.切片内存分配优化
GO的切片是动态数组,运行时动态增长.但是切片 的扩容可能会导致内存分配和拷贝.可以预先分配足够的内存大小.
切片的容量为 10,当我们追加第 11 个元素时,Go 会将切片的容量加倍,变为 20,并将原有的 10 个元素拷贝到新的内存位置。
例如:
slice := make([]int, 0 ,100)
四.避免过多的Map键对象
在GO中使用Map对象,过多的键会导致map的扫描时间增加从而影响性能.
- 优化哈希函数:设计高效的哈希函数,减少冲突。
- 预分配容量:使用
make
函数预分配map
的容量,避免频繁扩容。 - 分段存储:将数据分散到多个子
map
中,降低单个map
的元素数量。 - 监控性能:使用工具(如
pprof
)监控map
的性能,及时发现问题。
五.变量复用和对象池
优化内存的关键是减少对象的分配.主要采用一下方法实现变量复用和对象池
- 变量复用:在循环内部避免创建新对象.在循环外面定义变量和重复使用他们.
- 使用sync.Pool:对于需要频繁创建临时的对象,可以使用sync.Pool来维护对象池.在需要时从对象池中获取,使用完毕后放回.
package main
import (
"fmt"
"sync"
)
// 定义一个对象结构体
type Data struct {
ID int
Name string
}
func main() {
// 创建一个 sync.Pool 来管理 Data 对象
var pool = sync.Pool{
// New 函数用于创建新对象,如果池中没有可用对象时
New: func() interface{} {
// 创建一个新的 Data 对象
return &Data{}
},
}
// 获取一个对象,第一次会调用 New 函数
data1 := pool.Get().(*Data) // 从池中获取对象并断言类型
data1.ID = 1
data1.Name = "Object 1"
fmt.Printf("获取对象: ID=%d, Name=%s\n", data1.ID, data1.Name)
// 将对象放回池中,等待下次重用
pool.Put(data1)
// 获取一个对象,池中已有可用对象
data2 := pool.Get().(*Data)
fmt.Printf("获取对象: ID=%d, Name=%s\n", data2.ID, data2.Name)
// 将对象放回池中
data2.ID = 2
data2.Name = "Object 2"
pool.Put(data2)
// 获取一个对象,池中已有可用对象
data3 := pool.Get().(*Data)
fmt.Printf("获取对象: ID=%d, Name=%s\n", data3.ID, data3.Name)
}
六.增大GOGC的值.就是根据需要调整goGC机制在中设定的GC的内存值(常量)