吴云华 / Go中值类型和引用类型

作者:wuyunhua | 2023-09-16

这篇文章所讲述的值/引用类型,是golang开发人员必须清晰掌握,但又容易被忽略的知识。本文不讲原理,只讲实践。

哪些是值类型?哪些是引用类型?

chanmapsliceinterfacepointer是引用类型,其他都是值类型。

package main

import (
    "fmt"
)

func main() {
    // int、float、bool、string、array、struct都是值类型
    var a int = 1
    var b int = a
    b = 2
    fmt.Println(a, b) // 1 2

    // slice是引用类型
    var c []int = []int{1, 2, 3}
    var d []int = c
    d[0] = 4
    fmt.Println(c, d) // [4 2 3] [4 2 3]

    // map是引用类型
    var e map[string]int = map[string]int{"a": 1, "b": 2}
    var f map[string]int = e
    f["a"] = 3
    fmt.Println(e, f) // map[a:3 b:2] map[a:3 b:2]

    // chan是引用类型
    var g chan int = make(chan int, 1)
    var h chan int = g
    h <- 1
    fmt.Println(<-g, <-h) // 1 1

    // interface是引用类型
    var i interface{} = 1
    var j interface{} = i
    j = 2
    fmt.Println(i, j) // 1 2

    // 指针是引用类型
    var k *int = &a
    var l *int = k
    *l = 3
    fmt.Println(a, *k, *l) // 3 3 3
}

函数传参

函数传递参数是值传递,这意味着当你将一个变量传递给函数时,实际上是将该变量的副本传递给了函数,而不是原始变量本身。但是如果参数是引用类型,那么传递的是引用的副本,也就是说,函数内部修改了引用类型的值,那么函数外部的引用类型的值也会被修改。

package main

import "fmt"

func main() {
    value := 5
    fmt.Println(value) // 5

    // 传递的是值的副本
    func(value int) {
        value = 10
    }(value)
    fmt.Println(value) // 5

    // 传递的是引用的副本
    func(value *int) {
        *value = 10
    }(&value)
    fmt.Println(value) // 10
}

for range循环

截止当前版本1.21,在 for range 循环中,迭代变量的内存地址不会发生变化,它会在每次迭代中被重用,而不是为每次迭代创建一个新的变量

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}

    // v的内存地址不会发生变化
    for _, v := range slice {
        fmt.Printf("%p\n", &v) // 0xc0000b4008 0xc0000b4008 0xc0000b4008
    }

    for _, v := range slice {
        v := v
        fmt.Printf("%p\n", &v) // 0xc0000aa018 0xc0000aa030 0xc0000aa038
    }
}

关于结构体

结构体是使用频率较高的类型,需要强调结构体是值类型,但是结构体字段可以是引用类型。

package main

import "fmt"

type User struct {
    Name string
    Age  int
    // 字段可以是引用类型
    Friends []string
}

func main() {
    var user User
    // 结构体是值类型
    fmt.Println(user == nil) // invalid operation: user == nil (mismatched types User and nil)
    fmt.Println(user == User{}) // invalid operation: user == User{} (struct containing []string cannot be compared)
    fmt.Println(user.Name) // ""

    // 结构体字段可以是引用类型
    user.Friends = append(user.Friends, "Tom")
    var target = user
    target.Friends[0] = "Jerry"
    fmt.Println(user.Friends) // ["Jerry"]
}

切片的引用失效

slice的引用有限制,slice的cap发生变化时,引用会失效。

package main

import "fmt"

func main() {
    s1 := make([]int, 0, 10)
    s1 = []int{1, 2, 3} // <= cap现在是3
    s2 := s1
    s2 = append(s2, 4) // <= cap现在是6
    s2[0] = 100
    fmt.Println(s1, s2) // [1 2 3] [100 2 3 4]
}

容易出错的地方以及例子

for range循环中使用协程,很容易出错,因为迭代变量的内存地址不会发生变化,它会在每次迭代中被重用,而不是为每次迭代创建一个新的变量。

package main

import (
    "log"
    "sync"
)

type User struct {
    Name string
}

func main() {
    users := []User{
        {"a1"},
        {"a2"},
    }
    wg := sync.WaitGroup{}
    for _, user := range users {
        wg.Add(1)
        go func() {
            defer wg.Done()
            log.Printf("%p\n", &user) // 0xc000118230 0xc000118230
        }()
    }
    wg.Wait()
}

sync.Map中的引用类型,也是会被影响的

package main

import (
    "fmt"
    "sync"
)

type User struct {
    Name string
}

func main() {
    var s sync.Map
    user := &User{Name: "a1"}
    s.Store("a1", user)
    // 存储之后再修改
    user.Name = "a2"
    name, _ := s.Load("a1")
    fmt.Println(name.(*User).Name) // a2
}

为什么需要准确知道类型是值类型还是引用类型?

了解一个数据类型是值类型还是引用类型非常重要,因为它直接影响了如何操作和处理数据,以及在内存中如何存储和传递数据。

  • 值类型和引用类型在内存管理和性能方面有很大的不同。值类型通常存储在栈上,而引用类型通常存储在堆上。了解数据类型的存储位置可以帮助你更好地理解内存管理、分配和回收的机制,并更有效地处理大型数据结构。
  • 值类型的数据传递通常是按值传递,这意味着操作没有副作用,引用类型意味着可能有多个变量引用同一块内存,操作会有副作用。
  • 值类型通常需要在赋值或传递时进行拷贝,这可能导致性能开销,特别是对于大型数据结构。引用类型通常不会进行拷贝,因为多个变量可以共享相同的数据。

总结

了解数据类型是值类型还是引用类型对于编写高效、可维护和可预测的代码非常重要。不同的数据类型有不同的行为和特点,因此正确地理解和使用它们有助于避免潜在的问题和错误。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.wuyunhua.cn/golang-value-and-reference-types