Go

Golang的特性六(值传递与指针传递)----参数传递机制

Royal
2023-07-29 / 0 评论 / 25 阅读 / 正在检测是否收录...

在 Go 语言开发过程中,我们常常会面临一个抉择:在函数调用时,究竟应该选择值传递还是指针传递?这不仅关乎程序的性能,更影响着数据的安全性和一致性。
go允许通过指针(有时称为引用)和值来传递参数。在这篇文章中,我们将比较两种方法,特别注意可能影响选择的不同情境。

1. go中的传递本质
在 Go 语言里,只有值传递。所谓的引用传递,实际上也是传值,只不过拷贝的对象是一个地址,这个地址指向了另一个值的存储位置。所以,我们可以将问题转换为:到底该传值还是该传地址?

2. 如何选择传递方法是

- 遵循项目规范
项目规范文档是我们的首要依据。如果文档中对函数参数传递方式有明确规定,那么按照文档执行即可。例如,在调用第三方 SDK 时,很多简单类型(如 int、字符串类型等)的参数传递往往遵循特定规范,可能会要求传地址,这背后是为了满足接口设计的一致性和效率需求。

- 必须传地址的情况
当需要修改原始数据时,必须传递地址。如果传入值的副本,在函数内部对副本的修改将无法影响到原始数据。例如:

package main

import "fmt"

func modifyValue(ptr *int) {
    *ptr = 100
}

func main() {
    num := 50
    modifyValue(&num)
    fmt.Println(num) // 输出:100
}

- 必须传值的情况
若参数仅参与计算,且不希望其被修改,则应选择值传递。这样可以确保原始数据的安全性。例如:

package main

import "fmt"

func calculateValue(num int) int {
    return num * 2
}

func main() {
    originalNum := 5
    result := calculateValue(originalNum)
    fmt.Println(originalNum) // 输出:5
    fmt.Println(result)      // 输出:10
}

- 可传值可传地址的情况

  • 默认传值:在大多数情况下,默认选择值传递即可。例如传递简单的结构体或基本数据类型,值传递可以避免意外修改数据,使代码逻辑更加清晰。
  • 特殊情况传地址:对于大型数据结构(如包含多个字段的用户信息结构体),或者在多个模块中频繁调用的对象,如果进行值传递会导致大量的拷贝开销,此时可以考虑传递地址。例如:

    package main
    
    import "fmt"
    
    type User struct {
        Name    string
        Age     int
        Address string
    }
    
    func modifyUser(u *User) {
        u.Name = "New Name"
    }
    
    func main() {
        user := User{Name: "Old Name", Age: 25, Address: "123 Main St"}
        modifyUser(&user)
        fmt.Println(user.Name) // 输出:New Name
    }
    

3. 值类型与引用类型

  • 值类型
    常见的值类型包括数字类型(如 int、float、double 等)、数组、指针(虽然指针指向内存地址,但它本身是值类型)、字符串等。值类型在传递时会创建副本,对副本的修改不会影响原始数据。例如:

    package main
    
    import "fmt"
    
    func modifyArray(arr [3]int) {
        arr[0] = 100
    }
    
    func main() {
        originalArray := [3]int{1, 2, 3}
        modifyArray(originalArray)
        fmt.Println(originalArray) // 输出:[1 2 3]
    }
    
  • 引用类型
    引用类型包括切片、集合、channel 通道、函数类型等。引用类型在传递时,实际上传递的是指向底层数据结构的指针,因此对其修改会影响原始数据。例如:

    package main
     
    import "fmt"
     
    func modifySlice(slice []int) {
        slice[0] = 100
    }
     
    func main() {
        originalSlice := []int{1, 2, 3}
        modifySlice(originalSlice)
        fmt.Println(originalSlice) // 输出:[100 2 3]
    }

4. 深浅拷贝

  • 浅拷贝:浅拷贝只是拷贝对象本身,对于对象中引用的数据(如切片中的数组地址)不会进行拷贝。这意味着修改拷贝后的对象可能会影响原始数据,但拷贝成本较低。例如:

    package main
    
    import "fmt"
    
    func shallowCopySlice(slice []int) []int {
        newSlice := slice
        newSlice[0] = 100
        return newSlice
    }
    
    func main() {
        originalSlice := []int{1, 2, 3}
        copiedSlice := shallowCopySlice(originalSlice)
        fmt.Println(originalSlice) // 输出:[100 2 3]
        fmt.Println(copiedSlice)   // 输出:[100 2 3]
    }
    
  • 深拷贝:深拷贝不仅拷贝对象本身,还会递归地拷贝对象中引用的数据。这样可以确保修改拷贝后的对象不会影响原始数据,但拷贝成本较高。在 Go 语言中,通常需要手动实现深拷贝逻辑。

5. 方法接收器(Receiver)的选择
对于方法接收器(Receiver),如果没有规范要求,建议统一使用指针类型(*T)。这样做的好处是可以兼容值类型和指针类型的调用,并且在结构有方法且可能被多个位置频繁引用时,使用指针类型可以减少数据拷贝开销。例如:

package main
 
import "fmt"
 
type Cat struct {
    Name string
}
 
func (c *Cat) SetName(name string) {
    c.Name = name
}
 
func (c Cat) GetName() string {
    return c.Name
}
 
func main() {
    cat := Cat{Name: "Kitty"}
    cat.SetName("Tom")
    fmt.Println(cat.GetName()) // 输出:Tom
 
    // 使用指针调用方法
    ptrCat := &cat
    ptrCat.SetName("Jerry")
    fmt.Println(cat.GetName()) // 输出:Jerry
}
0

评论

博主关闭了当前页面的评论