MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go实参与形参传递的艺术

2021-01-118.0k 阅读

Go语言参数传递基础概念

在Go语言中,理解实参与形参的传递机制对于编写高效、可靠的代码至关重要。实参(实际参数)是在函数调用时传递给函数的值,而形参(形式参数)是函数定义中用于接收实参的变量。

Go语言中参数传递的基本方式是值传递。这意味着当函数被调用时,实参的值会被复制给形参。对于基本数据类型(如整数、浮点数、布尔值、字符串等),这种复制操作是直接进行的。例如:

package main

import "fmt"

func add(a int, b int) int {
    return a + b
}

func main() {
    x := 5
    y := 3
    result := add(x, y)
    fmt.Println("The result of addition is:", result)
}

在上述代码中,add 函数定义了两个形参 ab,它们接收 main 函数中传递的实参 xy。这里 xy 的值被复制给 ab,函数内部对 ab 的操作不会影响到 xy 的原始值。

基本数据类型的传递

整数类型

对于整数类型(如 intint8int16 等),值传递的过程非常直接。例如:

package main

import "fmt"

func increment(num int) {
    num = num + 1
    fmt.Println("Inside function, num is:", num)
}

func main() {
    value := 10
    fmt.Println("Before function call, value is:", value)
    increment(value)
    fmt.Println("After function call, value is:", value)
}

increment 函数中,numvalue 的一个副本。对 num 的修改不会影响到 main 函数中的 value。输出结果会显示 Before function call, value is: 10Inside function, num is: 11After function call, value is: 10

浮点数类型

浮点数类型(如 float32float64)的传递机制与整数类型相同。例如:

package main

import "fmt"

func multiply(f float64) {
    f = f * 2
    fmt.Println("Inside function, f is:", f)
}

func main() {
    num := 3.14
    fmt.Println("Before function call, num is:", num)
    multiply(num)
    fmt.Println("After function call, num is:", num)
}

这里 multiply 函数接收 num 的副本 f,对 f 的操作不影响 num 的原始值。

布尔类型

布尔类型 bool 的传递也是值传递。例如:

package main

import "fmt"

func toggle(b bool) {
    b =!b
    fmt.Println("Inside function, b is:", b)
}

func main() {
    flag := true
    fmt.Println("Before function call, flag is:", flag)
    toggle(flag)
    fmt.Println("After function call, flag is:", flag)
}

toggle 函数中,bflag 的副本,对 b 的取反操作不会影响到 main 函数中的 flag

字符串类型

字符串在Go语言中是不可变的,并且也是通过值传递。例如:

package main

import "fmt"

func appendString(s string) {
    s = s + " appended"
    fmt.Println("Inside function, s is:", s)
}

func main() {
    str := "original"
    fmt.Println("Before function call, str is:", str)
    appendString(str)
    fmt.Println("After function call, str is:", str)
}

appendString 函数中,sstr 的副本,对 s 的修改不会影响到 main 函数中的 str

复合数据类型的传递

数组

数组在Go语言中是值类型,当作为参数传递时,整个数组会被复制。例如:

package main

import "fmt"

func modifyArray(arr [3]int) {
    arr[0] = 100
    fmt.Println("Inside function, arr is:", arr)
}

func main() {
    numbers := [3]int{1, 2, 3}
    fmt.Println("Before function call, numbers is:", numbers)
    modifyArray(numbers)
    fmt.Println("After function call, numbers is:", numbers)
}

modifyArray 函数中,arrnumbers 的副本。对 arr 的修改不会影响到 main 函数中的 numbers。输出结果会显示 Before function call, numbers is: [1 2 3]Inside function, arr is: [100 2 3]After function call, numbers is: [1 2 3]

切片

切片在Go语言中是引用类型。虽然参数传递仍然是值传递,但传递的是切片的描述符,其中包含指向底层数组的指针、切片的长度和容量。这意味着通过切片参数对底层数组的修改会反映在原始切片上。例如:

package main

import "fmt"

func appendToSlice(slice []int) {
    slice = append(slice, 4)
    fmt.Println("Inside function, slice is:", slice)
}

func main() {
    mySlice := []int{1, 2, 3}
    fmt.Println("Before function call, mySlice is:", mySlice)
    appendToSlice(mySlice)
    fmt.Println("After function call, mySlice is:", mySlice)
}

appendToSlice 函数中,对 sliceappend 操作会修改底层数组(如果需要会重新分配内存),并且这种修改会反映在 main 函数中的 mySlice 上。输出结果会显示 Before function call, mySlice is: [1 2 3]Inside function, slice is: [1 2 3 4]After function call, mySlice is: [1 2 3 4]

映射

映射(map)也是引用类型。当作为参数传递时,传递的是指向映射数据结构的指针。这意味着在函数内部对映射的修改会影响到原始映射。例如:

package main

import "fmt"

func addToMap(m map[string]int) {
    m["newKey"] = 100
    fmt.Println("Inside function, m is:", m)
}

func main() {
    myMap := make(map[string]int)
    myMap["key1"] = 10
    fmt.Println("Before function call, myMap is:", myMap)
    addToMap(myMap)
    fmt.Println("After function call, myMap is:", myMap)
}

addToMap 函数中,对 m 的修改会直接影响到 main 函数中的 myMap。输出结果会显示 Before function call, myMap is: map[key1:10]Inside function, m is: map[key1:10 newKey:100]After function call, myMap is: map[key1:10 newKey:100]

结构体

结构体在Go语言中是值类型,当作为参数传递时,整个结构体被复制。例如:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func modifyPerson(p Person) {
    p.Name = "New Name"
    p.Age = 25
    fmt.Println("Inside function, p is:", p)
}

func main() {
    person := Person{Name: "Original Name", Age: 20}
    fmt.Println("Before function call, person is:", person)
    modifyPerson(person)
    fmt.Println("After function call, person is:", person)
}

modifyPerson 函数中,pperson 的副本。对 p 的修改不会影响到 main 函数中的 person。输出结果会显示 Before function call, person is: {Original Name 20}Inside function, p is: {New Name 25}After function call, person is: {Original Name 20}

指针类型的传递

指针在Go语言中允许对变量进行间接访问。当指针作为参数传递时,传递的是指针的值(即内存地址),通过指针可以修改指针所指向的变量的值。例如:

package main

import "fmt"

func incrementPtr(ptr *int) {
    *ptr = *ptr + 1
    fmt.Println("Inside function, value at ptr is:", *ptr)
}

func main() {
    num := 10
    fmt.Println("Before function call, num is:", num)
    incrementPtr(&num)
    fmt.Println("After function call, num is:", num)
}

incrementPtr 函数中,ptr 是指向 num 的指针的副本。通过 *ptr 可以修改 num 的值。输出结果会显示 Before function call, num is: 10Inside function, value at ptr is: 11After function call, num is: 11

传递大对象的性能考量

当传递大的数组或结构体时,由于值传递会复制整个对象,可能会导致性能问题。例如,传递一个非常大的数组:

package main

import "fmt"

func processLargeArray(arr [1000000]int) {
    // 简单处理,比如求和
    sum := 0
    for _, v := range arr {
        sum += v
    }
    fmt.Println("Sum of array inside function:", sum)
}

func main() {
    largeArray := [1000000]int{}
    for i := 0; i < 1000000; i++ {
        largeArray[i] = i
    }
    fmt.Println("Starting to process large array...")
    processLargeArray(largeArray)
    fmt.Println("Finished processing large array.")
}

在这个例子中,传递 largeArray 会复制整个数组,这在内存和时间上都是昂贵的操作。为了避免这种性能问题,可以考虑传递指针。例如:

package main

import "fmt"

func processLargeArrayPtr(ptr *[1000000]int) {
    // 简单处理,比如求和
    sum := 0
    for _, v := range *ptr {
        sum += v
    }
    fmt.Println("Sum of array inside function:", sum)
}

func main() {
    largeArray := [1000000]int{}
    for i := 0; i < 1000000; i++ {
        largeArray[i] = i
    }
    fmt.Println("Starting to process large array...")
    processLargeArrayPtr(&largeArray)
    fmt.Println("Finished processing large array.")
}

通过传递指针,只需要复制一个指针的大小(通常是 4 字节或 8 字节,取决于系统架构),而不是整个数组,从而显著提高性能。

传递函数作为参数

在Go语言中,函数也是一种类型,可以作为参数传递给其他函数。这被称为高阶函数。例如:

package main

import "fmt"

func operate(a, b int, f func(int, int) int) int {
    return f(a, b)
}

func add(a, b int) int {
    return a + b
}

func multiply(a, b int) int {
    return a * b
}

func main() {
    result1 := operate(3, 4, add)
    result2 := operate(3, 4, multiply)
    fmt.Println("Addition result:", result1)
    fmt.Println("Multiplication result:", result2)
}

operate 函数中,f 是一个函数类型的参数。operate 函数可以根据传入的不同函数 f 来执行不同的操作。这里分别传入了 addmultiply 函数,实现了不同的运算。

可变参数

Go语言支持可变参数,允许函数接受不定数量的参数。可变参数在函数定义中使用 ... 语法。例如:

package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

func main() {
    result1 := sum(1, 2, 3)
    result2 := sum(10, 20, 30, 40)
    fmt.Println("Sum 1:", result1)
    fmt.Println("Sum 2:", result2)
}

sum 函数中,nums 是一个 int 类型的切片,包含了所有传入的可变参数。函数可以对这些参数进行统一处理,如上述代码中的求和操作。

实参与形参传递的最佳实践

  1. 理解数据类型:在编写函数时,要清楚参数的数据类型及其传递方式。对于基本数据类型,值传递通常是直观的,但对于复合数据类型,如切片、映射和指针,要注意其引用特性,避免意外修改。
  2. 性能优化:当传递大对象时,优先考虑传递指针,以减少内存复制和提高性能。但要注意指针操作的安全性,避免空指针引用等问题。
  3. 函数签名清晰:函数的形参列表应该清晰地表达函数的意图。如果函数接受函数类型的参数,要明确说明该函数的预期行为和参数要求。
  4. 可变参数的使用:在使用可变参数时,要确保函数的逻辑能够正确处理不同数量的参数。同时,要注意可变参数与固定参数的组合使用,避免混淆。

总之,深入理解Go语言实参与形参的传递机制,能够帮助开发者编写出更高效、更健壮的代码。通过合理运用不同的数据类型和传递方式,可以优化程序的性能,同时确保程序的正确性和可读性。在实际开发中,不断实践和总结经验,能够更好地掌握这一重要的编程技巧。