Skip to content

基础语法

结构体能不能比较

在 Go 语言中,结构体可以进行比较,但有一些注意事项。

  1. 可比较的结构体:如果结构体的所有字段都是可比较的,那么结构体本身也是可比较的。这意味着你可以使用 ==!= 运算符来比较两个结构体变量是否具有相同的值。

    type Point struct {
        X, Y int
    }
    
    p1 := Point{1, 2}
    p2 := Point{1, 2}
    p3 := Point{2, 3}
    
    fmt.Println(p1 == p2) // 输出:true
    fmt.Println(p1 == p3) // 输出:false
    
  2. 不可比较的结构体:如果结构体包含不可比较的字段(例如 map 或 slice),则不能直接比较这些结构体。尝试这样做会在编译时导致错误。

newmake 的区别

在 Go 语言中,newmake 都是创建值的内建函数,但它们的用途和行为是有所不同的

  1. 用途

    • new 用于为任何类型分配内存,并返回该类型的零值的指针。
    • make 用于创建 slice、map 和 channel。
  2. 返回值

    • new(T) 返回一个指向类型 T 的新分配的零值的指针,返回的值是 *T 类型。
    • make(T) 返回一个已初始化的类型 T 的值,这意味着它返回的是相同的类型,不是指针。
  3. 零值和初始化

    • new 分配内存并返回指向该内存的指针,这块内存被初始化为类型的零值。
    • make 创建一个具有非零值的值。例如,创建一个切片时,它将为切片分配底层数组并准备好切片的长度和容量。
  4. 用法示例

    • 使用 new
      ptr := new(int)
      *ptr = 3
      
    • 使用 make
      slice := make([]int, 3)  // 创建一个长度为3的int切片
      mapData := make(map[string]int)  // 创建一个string到int的映射
      ch := make(chan int)  // 创建一个int类型的通道
      

切片和数组的区别

在 Go 语言中,切片(slice)和数组(array)都是处理序列数据的重要数据结构

  1. 大小固定性

    • 数组:当定义数组时,需要指定其大小,而且该大小在整个生命周期中是固定的。一旦数组被定义,不能更改其大小。
    • 切片:切片的大小是动态的。你可以向切片追加元素,使其增长,但内部它是通过重新分配一个更大的底层数组来实现的。
  2. 声明方式

    • 数组:指定其长度。
      var arr [5]int
      
    • 切片:不指定长度。
      var s []int
      
  3. 长度与容量

    • 数组:长度就是其声明时指定的大小。
    • 切片:具有长度和容量两个属性。长度是切片中当前元素的数量,而容量是底层数组中的元素数量。由于切片是引用底层数组的,所以其容量可能大于其长度。
  4. 底层实现

    • 数组:是一个连续的内存块,由固定数目的元素组成。
    • 切片:是一个引用类型,它有三个关键属性:指向底层数组的指针、长度和容量。
  5. 函数参数传递

    • 数组:作为函数参数传递时,是值传递,函数内对数组的修改不会影响原数组。
    • 切片:作为函数参数传递时,是引用传递。函数内对切片的修改会影响原切片。

切片是如何扩容的?

  1. 扩容触发:当你使用 append 向切片添加元素,并且切片的当前容量不足以容纳新的元素时,扩容会被触发。

  2. 扩容策略

    • 对于容量小于 256 的切片,新容量通常是当前容量的两倍。

    • 对于容量大于或等于 256 的切片,\(\rm{newcap = oldcap+(oldcap+3 \times 256)/4}\)

    • 但是,考虑到内存对齐,新容量会比上面的公式计算出来的容量更大

    • 以上是 Go 1.18 及其之后版本的扩容策略,在旧版本中,扩容阈值和增长因子会有变化

  3. 扩容实现:创建一个新的底层数组、复制旧的数据,然后更新切片的引用以指向新数组。无其他引用的旧底层数组将被垃圾收集。

  4. 关于底层数组的共享:当从一个切片创建另一个切片时,它们可能会共享同一个底层数组。因此,在扩容之前,对其中一个切片的修改可能会影响到另一个切片。但一旦触发扩容,切片将引用一个新的底层数组,此时原切片和新切片就不再共享数据了。

race 检测是什么?

为了检测数据竞争,Go 提供了一个工具叫做 race detector

以下是如何使用 race detector 的简要指南:

  1. 编译/运行代码时使用 -race 选项

    当编译、测试或运行你的程序时,加上 -race 选项。

    go run -race your_program.go
    

    go test -race ./...
    
  2. 分析输出

    如果 race detector 检测到数据竞争,它将在标准输出中打印相关的警告,包括竞争发生的位置和涉及的 goroutines。

  3. 解决数据竞争

    如果你发现了数据竞争,通常你需要引入同步原语,例如 sync.Mutexsync.RWMutex,来保护对数据的访问。另一种方法是使用并发安全的数据结构。

    示例:下面是一个简单的例子,展示了一个并发访问 map 的数据竞争。

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        var wg sync.WaitGroup
        m := make(map[int]int)
    
        wg.Add(2)
    
        go func() {
            for i := 0; i < 1000; i++ {
                m[i] = i
            }
            wg.Done()
        }()
    
        go func() {
            for i := 0; i < 1000; i++ {
                fmt.Println(m[i])
            }
            wg.Done()
        }()
    
        wg.Wait()
    }
    

    如果你使用 race detector 运行上面的代码,你会看到一个关于数据竞争的警告。

defer 的使用场景和原理?

使用场景

defer 语句在 Go 语言中用于确保函数调用在正常执行路径(包括出现错误时)结束后能被执行。主要使用场景包括:

  1. 资源清理:如关闭文件句柄、数据库连接、网络连接等。通过 defer 确保资源无论如何都会被释放,防止资源泄露。

    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close() // 确保文件关闭
    
  2. 锁的释放:在处理并发时,确保互斥锁(mutex)在函数结束时被释放。

    mu.Lock()
    defer mu.Unlock() // 确保锁被释放
    //... do something ...
    
  3. 延迟执行代码:对于需要在函数结束时执行的代码,如日志记录、发送通知、错误处理等。

    defer log.Println("Function has been executed")
    
  4. 复杂逻辑的清理代码:简化有多个返回点的函数的清理代码。

    func complexFunction() (err error) {
        res, err := doSomething()
        if err != nil {
            return err
        }
        defer res.Cleanup() // 清理资源
        // 更多逻辑
    }
    

原理

defer 的工作原理和执行机制如下:

  1. 后进先出(LIFO)顺序defer 语句遵循 LIFO 顺序。最后一个被 defer 的语句将首先执行。

  2. 参数立即求值:在 defer 语句被执行时,所有函数参数都会被立即求值。但是 defer 语句中的函数本身直到包含它的函数执行结束前才被调用。

  3. 函数作用域内有效defer 语句仅在定义它的函数的作用域内有效。

  4. 返回值影响:如果在 defer 语句中使用了命名返回值,它可以修改这些返回值。

  5. panic处理defer 语句即使在发生 panic 时也会执行,因此它们常用于恢复(recover)操作。

  6. 执行时机defer 语句在包含它的函数执行的 return 语句之后、返回值返回给调用者之前执行。

defer 的这些特性使得它在资源管理和错误处理方面非常有用,帮助编写清晰和可维护的代码。

如果我要在 defer 里面修改 return 里面的值呢?这时怎么写?

如果你想在 defer 语句中修改函数的返回值,你可以使用命名返回值。在函数声明中为返回值命名后,这个命名变量在整个函数范围内都是有效的,包括 defer 语句中。

这里有个例子来展示如何在 defer 中修改返回值:

func example() (result int) {
    defer func() {
        // 修改返回值
        result++
    }()

    // 设置返回值
    result = 3

    return // 这里实际返回的将是4
}

func main() {
    fmt.Println(example()) // 输出:4
}

在这个例子中,函数 example 有一个命名返回值 result。在 defer 函数中,我们增加了 result 的值。当 example 函数返回时,由于 defer 已经修改了 result 的值,所以实际返回的值是修改后的值。