当前位置: 首页 > news >正文

Go语言内存管理深度解析:堆栈分配与逃逸分析的艺术

📚 前言

在现代软件开发中,内存管理是决定程序性能的关键因素之一。Go语言以其优雅的内存管理机制闻名,它通过自动垃圾回收(GC)解放了开发者手动管理内存的负担,同时通过逃逸分析等编译时优化技术,确保了程序的高性能运行。

你是否曾经好奇过:

  • 为什么有些变量分配在栈上,有些却在堆上?
  • Go编译器是如何决定内存分配位置的?
  • 如何优化代码以减少不必要的堆分配?

本文将带你深入探索Go语言的内存管理机制,重点剖析堆栈分配原理和逃逸分析技术,帮助你写出更高效的Go代码。

📑 目录

  1. 内存管理基础:栈与堆的世界
  2. Go语言的内存分配策略
  3. 逃逸分析:编译器的智慧
  4. 常见的逃逸场景分析
  5. 性能优化实战
  6. 调试工具与技巧
  7. 最佳实践与陷阱规避
  8. 总结与展望

1. 内存管理基础:栈与堆的世界

1.1 栈(Stack)内存特性

栈是一种后进先出(LIFO)的内存结构,每个goroutine都有自己独立的栈空间。

package mainimport "fmt"func stackExample() {// 这些变量都分配在栈上x := 42           // 基本类型y := 3.14         // 浮点数arr := [3]int{1, 2, 3}  // 固定大小的数组fmt.Printf("x的地址: %p\n", &x)fmt.Printf("y的地址: %p\n", &y)fmt.Printf("arr的地址: %p\n", &arr)
}func main() {stackExample()// 函数返回后,栈上的变量自动释放
}

栈内存的特点:

  • 分配速度快:只需移动栈指针(SP)
  • 自动管理:函数返回时自动释放
  • 内存连续:CPU缓存友好,访问速度快
  • 大小受限:goroutine初始栈大小为2KB,最大可增长到1GB(64位系统)

1.2 堆(Heap)内存特性

堆是所有goroutine共享的内存区域,用于存储生命周期超出函数作用域的数据。

package mainimport ("fmt""runtime"
)type Person struct {Name stringAge  int
}func createPerson() *Person {// p会逃逸到堆上,因为返回了它的指针p := &Person{Name: "张三",Age:  25,}return p
}func main() {person := createPerson()fmt.Printf("Person: %+v\n", person)// 查看内存统计var m runtime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("堆内存分配: %d bytes\n", m.HeapAlloc)
}

堆内存的特点:

  • 动态分配:运行时按需分配
  • 生命周期灵活:可跨函数、跨goroutine访问
  • 需要GC管理:垃圾回收器负责清理
  • 分配成本高:涉及更复杂的内存管理算法

1.3 内存布局示意图

┌─────────────────────────────────────┐
│            Go 程序内存布局            │
├─────────────────────────────────────┤
│                                     │
│            Heap (堆)                │
│    ┌─────────────────────┐         │
│    │  动态分配的对象        │         │
│    │  逃逸的变量           │         │
│    │  大对象               │         │
│    │  ...                │         │
│    └─────────────────────┘         │
│                                     │
├─────────────────────────────────────┤
│         Goroutine 1 Stack          │
│    ┌─────────────────────┐         │
│    │  函数参数            │         │
│    │  局部变量            │         │
│    │  返回地址            │         │
│    └─────────────────────┘         │
├─────────────────────────────────────┤
│         Goroutine 2 Stack          │
│    ┌─────────────────────┐         │
│    │  函数参数            │         │
│    │  局部变量            │         │
│    │  返回地址            │         │
│    └─────────────────────┘         │
└─────────────────────────────────────┘

2. Go语言的内存分配策略

2.1 自动内存管理

Go语言采用自动内存管理,开发者无需手动调用malloc/free,编译器和运行时会自动决定内存分配位置。

package mainimport ("fmt""unsafe"
)func memoryAllocation() {// 编译器自动决定分配位置var x int = 100              // 栈分配var y = make([]int, 10)     // 小切片,可能栈分配var z = make([]int, 10000)  // 大切片,堆分配fmt.Printf("x 大小: %d bytes\n", unsafe.Sizeof(x))fmt.Printf("y 切片头大小: %d bytes\n", unsafe.Sizeof(y))fmt.Printf("z 切片头大小: %d bytes\n", unsafe.Sizeof(z))
}func main() {memoryAllocation()
}

2.2 内存分配决策流程

                    ┌──────────────┐│   变量声明    │└──────┬───────┘│┌──────▼───────┐│  逃逸分析     │└──────┬───────┘│┌──────────┴──────────┐│                     │┌───────▼────────┐   ┌───────▼────────┐│  不逃逸         │   │   逃逸          │└───────┬────────┘   └───────┬────────┘│                     │┌───────▼────────┐   ┌───────▼────────┐│   栈分配        │   │   堆分配        │└───────┬────────┘   └───────┬────────┘│                     │┌───────▼────────┐   ┌───────▼────────┐│ 函数返回自动释放 │   │   GC负责回收     │└────────────────┘   └────────────────┘

2.3 栈的动态增长

Go的栈是动态的,当栈空间不足时会自动扩容:

package mainimport ("fmt""runtime"
)func recursiveFunction(depth int) {if depth <= 0 {return}// 创建一个较大的局部数组,消耗栈空间var largeArray [1024]intlargeArray[0] = depth// 递归调用recursiveFunction(depth - 1)
}func main() {// 查看当前goroutine数量fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())// 触发栈增长recursiveFunction(100)fmt.Println("递归完成,栈已自动调整")
}

3. 逃逸分析:编译器的智慧

3.1 什么是逃逸分析?

逃逸分析(Escape Analysis)是Go编译器的一项重要优化技术,用于在编译时确定变量应该分配在栈上还是堆上。

package mainimport "fmt"// 不逃逸的例子
func sum(a, b int) int {result := a + b  // result不逃逸,分配在栈上return result
}// 逃逸的例子
func createSlice() *[]int {slice := make([]int, 10)  // slice逃逸到堆上return &slice
}func main() {// 分析逃逸情况s := sum(10, 20)fmt.Println("Sum:", s)slicePtr := createSlice()fmt.Printf("Slice: %v\n", *slicePtr)
}

3.2 逃逸分析的原则

Go编译器遵循以下基本原则进行逃逸分析:

  1. 指针逃逸原则:如果函数返回局部变量的指针,该变量逃逸到堆
  2. 栈空间不足原则:如果变量过大,超出栈的容量限制,逃逸到堆
  3. 动态类型原则:如果变量的类型在编译时无法确定(如interface{}),逃逸到堆
  4. 闭包引用原则:被闭包引用的局部变量逃逸到堆

3.3 逃逸分析决策树

                 ┌─────────────────┐│   分析变量 V     │└────────┬────────┘│┌─────────────▼─────────────┐│  V的引用是否逃出函数作用域?  │└─────────────┬─────────────┘│┌─────Yes───┴───No─────┐│                      │┌─────────▼────────┐   ┌────────▼────────┐│   逃逸到堆        │   │  V的大小是否过大? │└──────────────────┘   └────────┬────────┘│┌─────Yes───┴───No─────┐│                      │┌─────────▼────────┐   ┌────────▼────────┐│   逃逸到堆        │   │ V是否是动态类型? │└──────────────────┘   └────────┬────────┘│┌─────Yes───┴───No─────┐│                      │┌─────────▼────────┐   ┌────────▼────────┐│   逃逸到堆        │   │   分配在栈上     │└──────────────────┘   └─────────────────┘

4. 常见的逃逸场景分析

4.1 场景一:返回局部变量指针

package mainimport "fmt"type User struct {ID   intName string
}// 逃逸:返回局部变量的指针
func createUser() *User {u := User{ID: 1, Name: "Alice"}  // u逃逸到堆return &u
}// 不逃逸:返回值而非指针
func createUserValue() User {u := User{ID: 2, Name: "Bob"}    // u在栈上return u
}func main() {user1 := createUser()user2 := createUserValue()fmt.Printf("User1: %+v\n", user1)fmt.Printf("User2: %+v\n", user2)
}

4.2 场景二:Interface类型逃逸

package mainimport "fmt"func printInterface() {var x = 42var y interface{} = x  // x的值被装箱,逃逸到堆fmt.Println(y)
}// 优化版本:避免interface逃逸
func printDirect() {var x = 42fmt.Println(x)  // 直接打印,不逃逸
}func main() {printInterface()printDirect()
}

4.3 场景三:闭包引起的逃逸

package mainimport "fmt"func closureExample() func() int {x := 100  // x被闭包引用,逃逸到堆return func() int {x++return x}
}func main() {fn := closureExample()fmt.Println(fn())  // 输出: 101fmt.Println(fn())  // 输出: 102
}

4.4 场景四:大对象逃逸

package mainimport ("fmt""runtime"
)const (SmallSize = 64 * 1024    // 64KBLargeSize = 10 * 1024 * 1024  // 10MB
)func allocateSmall() {// 小对象可能在栈上small := make([]byte, SmallSize)small[0] = 1_ = small
}func allocateLarge() {// 大对象一定在堆上large := make([]byte, LargeSize)large[0] = 1_ = large
}func main() {var m1, m2 runtime.MemStatsruntime.ReadMemStats(&m1)allocateSmall()runtime.ReadMemStats(&m2)fmt.Printf("Small allocation - Heap增长: %d bytes\n", m2.HeapAlloc-m1.HeapAlloc)runtime.ReadMemStats(&m1)allocateLarge()runtime.ReadMemStats(&m2)fmt.Printf("Large allocation - Heap增长: %d bytes\n", m2.HeapAlloc-m1.HeapAlloc)
}

4.5 场景五:切片的逃逸行为

package mainimport "fmt"// 切片作为参数传递(不逃逸)
func processSlice(s []int) int {sum := 0for _, v := range s {sum += v}return sum
}// 返回切片(可能逃逸)
func createSlice() []int {return make([]int, 100)  // 逃逸到堆
}// 切片append导致的逃逸
func appendSlice() {s := make([]int, 0, 10)for i := 0; i < 20; i++ {s = append(s, i)  // 超过容量时重新分配,可能逃逸}fmt.Printf("Final length: %d\n", len(s))
}func main() {slice := []int{1, 2, 3, 4, 5}fmt.Println("Sum:", processSlice(slice))newSlice := createSlice()fmt.Println("New slice length:", len(newSlice))appendSlice()
}

5. 性能优化实战

5.1 减少不必要的堆分配

package mainimport ("fmt""testing"
)// 性能较差:频繁堆分配
type BadBuffer struct {data []byte
}func (b *BadBuffer) Write(p []byte) {b.data = append(b.data, p...)
}// 性能优化:使用对象池减少堆分配
import "sync"var bufferPool = sync.Pool{New: func() interface{} {return make([]byte, 0, 1024)},
}type GoodBuffer struct {data []byte
}func (g *GoodBuffer) Write(p []byte) {if g.data == nil {g.data = bufferPool.Get().([]byte)}g.data = append(g.data, p...)
}func (g *GoodBuffer) Reset() {if g.data != nil {g.data = g.data[:0]bufferPool.Put(g.data)g.data = nil}
}// 基准测试对比
func BenchmarkBadBuffer(b *testing.B) {data := []byte("Hello, World!")b.ResetTimer()for i := 0; i < b.N; i++ {buf := &BadBuffer{}for j := 0; j < 100; j++ {buf.Write(data)}}
}func BenchmarkGoodBuffer(b *testing.B) {data := []byte("Hello, World!")b.ResetTimer()for i := 0; i < b.N; i++ {buf := &GoodBuffer{}for j := 0; j < 100; j++ {buf.Write(data)}buf.Reset()}
}

5.2 值传递 vs 指针传递优化

package mainimport ("fmt""testing"
)// 小结构体 - 值传递可能更高效
type SmallStruct struct {X, Y int
}func processSmallByValue(s SmallStruct) int {return s.X + s.Y
}func processSmallByPointer(s *SmallStruct) int {return s.X + s.Y
}// 大结构体 - 指针传递更高效
type LargeStruct struct {Data [1000]intName stringTags []string
}func processLargeByValue(l LargeStruct) int {return len(l.Data)
}func processLargeByPointer(l *LargeStruct) int {return len(l.Data)
}// 性能测试
func BenchmarkSmallStructValue(b *testing.B) {s := SmallStruct{X: 10, Y: 20}b.ResetTimer()for i := 0; i < b.N; i++ {_ = processSmallByValue(s)}
}func BenchmarkSmallStructPointer(b *testing.B) {s := &SmallStruct{X: 10, Y: 20}b.ResetTimer()for i := 0; i < b.N; i++ {_ = processSmallByPointer(s)}
}func main() {// 实际使用建议fmt.Println("小结构体(<64字节):优先使用值传递")fmt.Println("大结构体(>64字节):优先使用指针传递")fmt.Println("包含锁的结构体:必须使用指针传递")
}

5.3 字符串优化技巧

package mainimport ("bytes""fmt""strings""testing"
)// 低效:字符串拼接导致大量堆分配
func badStringConcat(n int) string {s := ""for i := 0; i < n; i++ {s += "a"  // 每次都创建新字符串}return s
}// 高效方案1:使用strings.Builder
func goodStringBuilder(n int) string {var builder strings.Builderbuilder.Grow(n)  // 预分配容量for i := 0; i < n; i++ {builder.WriteString("a")}return builder.String()
}// 高效方案2:使用bytes.Buffer
func goodBytesBuffer(n int) string {var buffer bytes.Bufferbuffer.Grow(n)for i := 0; i < n; i++ {buffer.WriteString("a")}return buffer.String()
}// 基准测试
func BenchmarkBadStringConcat(b *testing.B) {for i := 0; i < b.N; i++ {_ = badStringConcat(100)}
}func BenchmarkGoodStringBuilder(b *testing.B) {for i := 0; i < b.N; i++ {_ = goodStringBuilder(100)}
}func main() {fmt.Println("字符串优化建议:")fmt.Println("1. 避免使用+进行大量字符串拼接")fmt.Println("2. 使用strings.Builder或bytes.Buffer")fmt.Println("3. 预先分配容量以减少内存重分配")
}

6. 调试工具与技巧

6.1 使用编译器flag查看逃逸分析

# 查看逃逸分析结果
go build -gcflags="-m" main.go# 查看更详细的逃逸分析
go build -gcflags="-m -m" main.go# 禁用内联并查看逃逸分析
go build -gcflags="-l -m" main.go

示例代码和分析:

package mainfunc example() *int {x := 42return &x  // 编译器会提示: moved to heap: x
}func main() {p := example()println(*p)
}

6.2 使用pprof进行内存分析

package mainimport ("fmt""net/http"_ "net/http/pprof""time"
)func memoryLeakSimulation() {// 模拟内存泄漏var leakySlice [][]byteticker := time.NewTicker(100 * time.Millisecond)defer ticker.Stop()for range ticker.C {// 不断分配内存但不释放data := make([]byte, 1024*1024) // 1MBleakySlice = append(leakySlice, data)if len(leakySlice) > 100 {break}}
}func main() {// 启动pprof服务器go func() {fmt.Println("pprof server listening on :6060")http.ListenAndServe(":6060", nil)}()// 模拟内存使用memoryLeakSimulation()fmt.Println("访问 http://localhost:6060/debug/pprof/heap 查看堆内存分析")// 保持程序运行select {}
}

使用pprof命令:

# 获取heap profile
go tool pprof http://localhost:6060/debug/pprof/heap# 在pprof交互界面中的常用命令
(pprof) top        # 查看内存占用最高的函数
(pprof) list main  # 查看main包的详细信息
(pprof) web        # 生成调用图(需要graphviz)

6.3 runtime包的内存统计

package mainimport ("fmt""runtime""time"
)func printMemStats(label string) {var m runtime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("\n=== %s ===\n", label)fmt.Printf("Alloc = %v MB", bToMb(m.Alloc))fmt.Printf("\tTotalAlloc = %v MB", bToMb(m.TotalAlloc))fmt.Printf("\tSys = %v MB", bToMb(m.Sys))fmt.Printf("\tNumGC = %v\n", m.NumGC)fmt.Printf("HeapAlloc = %v MB", bToMb(m.HeapAlloc))fmt.Printf("\tHeapSys = %v MB", bToMb(m.HeapSys))fmt.Printf("\tHeapIdle = %v MB", bToMb(m.HeapIdle))fmt.Printf("\tHeapInuse = %v MB\n", bToMb(m.HeapInuse))fmt.Printf("Stack = %v MB", bToMb(m.StackSys))fmt.Printf("\tMSpan = %v MB", bToMb(m.MSpanSys))fmt.Printf("\tMCache = %v MB\n", bToMb(m.MCacheSys))
}func bToMb(b uint64) uint64 {return b / 1024 / 1024
}func allocateMemory() {// 分配一些内存data := make([][]byte, 100)for i := range data {data[i] = make([]byte, 1024*1024) // 1MB each}time.Sleep(1 * time.Second)// 触发GCruntime.GC()
}func main() {printMemStats("程序启动")allocateMemory()printMemStats("分配内存后")runtime.GC()printMemStats("手动GC后")
}

7. 最佳实践与陷阱规避

7.1 最佳实践清单

1. 合理使用指针
// ✅ 好的实践:小结构体直接传值
func processPoint(p Point) float64 {return math.Sqrt(p.X*p.X + p.Y*p.Y)
}// ❌ 避免:小结构体不必要的指针传递
func processPointBad(p *Point) float64 {return math.Sqrt(p.X*p.X + p.Y*p.Y)
}// ✅ 好的实践:大结构体使用指针
func processLargeData(data *LargeStruct) {// 处理大型数据结构
}
2. 预分配切片容量
// ✅ 好的实践:预分配容量
func collectData(n int) []int {result := make([]int, 0, n)  // 预分配容量for i := 0; i < n; i++ {result = append(result, i)}return result
}// ❌ 避免:动态增长导致多次重分配
func collectDataBad(n int) []int {var result []int  // 容量为0for i := 0; i < n; i++ {result = append(result, i)  // 可能多次重分配}return result
}
3. 使用sync.Pool复用对象
import "sync"var bufferPool = sync.Pool{New: func() interface{} {return new(bytes.Buffer)},
}func processWithPool(data []byte) {buf := bufferPool.Get().(*bytes.Buffer)defer func() {buf.Reset()bufferPool.Put(buf)}()buf.Write(data)// 处理buffer
}

7.2 常见陷阱与解决方案

陷阱1:循环中的闭包变量
// ❌ 错误:所有goroutine共享同一个i变量
func badGoroutines() {for i := 0; i < 10; i++ {go func() {fmt.Println(i)  // 所有goroutine可能打印10}()}
}// ✅ 正确:每个goroutine有自己的i副本
func goodGoroutines() {for i := 0; i < 10; i++ {go func(n int) {fmt.Println(n)}(i)}
}
陷阱2:Map的内存泄漏
// ❌ 潜在问题:map永不收缩
type Cache struct {data map[string][]byte
}func (c *Cache) Set(key string, value []byte) {c.data[key] = value
}func (c *Cache) Delete(key string) {delete(c.data, key)  // map本身不会收缩
}// ✅ 解决方案:定期重建map
func (c *Cache) Cleanup() {if len(c.data) < cap(c.data)/2 {newData := make(map[string][]byte, len(c.data))for k, v := range c.data {newData[k] = v}c.data = newData}
}
陷阱3:defer在循环中的使用
// ❌ 问题:defer累积到函数结束
func badDefer() {for i := 0; i < 10000; i++ {file, _ := os.Open("test.txt")defer file.Close()  // 累积10000个defer// 处理文件}
}// ✅ 解决:使用匿名函数限制defer作用域
func goodDefer() {for i := 0; i < 10000; i++ {func() {file, _ := os.Open("test.txt")defer file.Close()  // 每次循环后立即执行// 处理文件}()}
}

7.3 性能优化决策树

                    ┌────────────────┐│  性能瓶颈分析   │└────────┬───────┘│┌────────▼───────┐│  使用pprof分析  │└────────┬───────┘│┌────────────┴────────────┐│                         │┌───────▼────────┐       ┌───────▼────────┐│   CPU瓶颈      │       │   内存瓶颈      │└───────┬────────┘       └───────┬────────┘│                         │┌───────▼────────┐       ┌───────▼────────┐│  优化算法       │       │  检查逃逸分析    ││  减少计算量     │       └───────┬────────┘└────────────────┘               │┌─────────┴─────────┐│                   │┌───────▼────────┐ ┌───────▼────────┐│ 减少堆分配      │ │  使用对象池     ││ 优化数据结构    │ │  预分配内存     │└────────────────┘ └────────────────┘

8. 总结与展望

8.1 核心要点回顾

通过本文的深入探讨,我们掌握了Go语言内存管理的核心概念:

  1. 栈与堆的本质区别

    • 栈:快速、自动管理、大小受限
    • 堆:灵活、需要GC、开销较大
  2. 逃逸分析的重要性

    • 编译时决定内存分配位置
    • 直接影响程序性能
    • 可通过编译器flag查看分析结果
  3. 常见逃逸场景

    • 返回指针
    • interface{}装箱
    • 闭包引用
    • 大对象分配
    • 动态类型
  4. 优化策略

    • 减少不必要的堆分配
    • 合理选择值传递vs指针传递
    • 使用sync.Pool复用对象
    • 预分配切片和map容量

8.2 性能优化黄金法则

// 记住这些原则,写出高性能Go代码// 1. 测量先于优化
// "过早优化是万恶之源" - Donald Knuth
// 使用benchmark和pprof找出真正的瓶颈// 2. 理解而非猜测
// 通过-gcflags="-m"理解编译器的决策// 3. 简单优于复杂
// 清晰的代码往往性能也不差// 4. 缓存友好
// 利用局部性原理,减少cache miss// 5. 并发不总是更快
// goroutine也有开销,合理控制并发度

8.3 Go内存管理的未来

随着Go语言的不断演进,内存管理也在持续优化:

  • 更智能的逃逸分析:编译器越来越聪明,能识别更多不需要逃逸的场景
  • 更高效的GC:GC延迟持续降低,吞吐量不断提升
  • 泛型带来的新机遇:Go 1.18+的泛型为内存优化提供了新的可能性

8.4 推荐学习资源

  1. 官方文档

    • Go Memory Model
    • Effective Go
  2. 深入学习

    • 《Go语言高级编程》
    • 《Go语言设计与实现》
    • Go源码阅读(runtime包)
  3. 工具使用

    • pprof性能分析
    • go-torch火焰图
    • benchstat基准测试对比

8.5 结语

Go语言的内存管理是一个深邃而迷人的领域。通过理解堆栈分配机制和逃逸分析原理,我们不仅能写出更高效的代码,还能更深刻地理解Go语言的设计哲学。

记住,性能优化是一个持续的过程,需要不断地测量、分析和改进。希望本文能成为你Go语言性能优化之旅的有力助手!


💡 关键要点总结

  • 栈分配优于堆分配:尽量让变量留在栈上
  • 理解逃逸分析:知道什么情况会导致逃逸
  • 合理使用工具:善用pprof和编译器flag
  • 持续学习:Go在不断进化,保持学习热情

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题或建议,请在评论区留言交流。

作者:小羊斩肖恩
更新时间:2025年8月
版权声明:本文为原创内容,转载请注明出处

http://www.dtcms.com/a/349894.html

相关文章:

  • 深度学习篇---ResNet家族
  • Matlab高光谱遥感、数据处理与混合像元分解实践技术应用
  • Mysql系列--8、索引
  • Kubernetes部署MySQL主从复制
  • PyTorch中 nn.Linear详解和实战示例
  • Java全栈开发实战:从基础到微服务的深度探索
  • [Python]库Pandas应用总结
  • PE嵌入式签名检测方法
  • 阿里开源Vivid-VR:AI视频修复新标杆,解锁内容创作新可能
  • AR远程协助:能源电力行业智能化革新
  • 一键编译安装zabbix(centos)
  • Spark面试题
  • HTTP 协议与TCP 的其他机制
  • excel 破解工作表密码
  • Python之Flask快速入门
  • Redis类型之List
  • 自然语言处理——07 BERT、ELMO、GTP系列模型
  • lesson46-1:Linux 常用指令全解析:从基础操作到高效应用
  • Docker:常用命令、以及设置别名
  • 数据挖掘 6.1 其他降维方法(不是很重要)
  • 聊聊负载均衡架构
  • 关于窗口关闭释放内存,主窗口下的子窗口关闭释放不用等到主窗口关闭>setAttribute(Qt::WA_DeleteOnClose);而且无需手动释放
  • 【Python】QT(PySide2、PyQt5):列表视图、模型、自定义委托
  • 【芯片后端设计的灵魂:Placement的作用与重要性】
  • SQL 语句拼接在 C 语言中的实现与安全性分析
  • 跨语言统一语义真理及其对NLP深层分析影响
  • 2.3零基础玩转uni-app轮播图:从入门到精通 (咸虾米总结)
  • Python 实战:内网渗透中的信息收集自动化脚本(3)
  • 苹果公司即将启动一项为期三年的计划
  • Linux应急响应一般思路(三)