跳转至

05 函数和方法:Go 语言中的函数和方法到底有什么不同?

在上节课的思考题中,要求创建一个二维数组并使用。二维数组的声明和一维数组非常相似:

package main

import "fmt"

func main() {
    // 声明一个 3 行 3 列的二维整型数组
    var aa [3][3]int
    aa[0][0] = 1
    aa[0][1] = 2
    aa[0][2] = 3
    aa[1][0] = 4
    aa[1][1] = 5
    aa[1][2] = 6
    aa[2][0] = 7
    aa[2][1] = 8
    aa[2][2] = 9

    fmt.Println(aa) // 输出: [[1 2 3] [4 5 6] [7 8 9]]
}

函数和方法是代码复用与职责分离的第一步。通过函数可以将复杂的开发任务拆解为小模块,提高协作效率与代码质量。

本课时将带你深入了解 Go 语言中函数和方法的声明、调用以及它们的核心差异。


1. 函数

1.1 函数初探与声明格式

在 Go 语言中,入口函数 main 是一个最典型的函数:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

通用函数声明语法格式如下:

func funcName(params) result {
    body
}
  1. func 关键字:用于声明函数;
  2. funcName 函数名:遵循 Go 标识符命名规范;
  3. params 入参列表:定义形参的变量名与类型,可以有零个或多个;
  4. result 返回值类型:定义返回值的类型,可为空、单个或多个;
  5. body 函数体:包含函数核心逻辑的大括号代码块。

定义一个计算两数之和的简单函数:

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

若连续多个形参类型相同,可以省略前面的类型声明:

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

main 函数中调用该函数:

package main

import "fmt"

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

func main() {
    result := sum(1, 2)
    fmt.Println(result) // 输出: 3
}

1.2 多值返回

Go 语言支持原生 多值返回。通常第一个值返回实际结果,第二个值返回错误信息 error

示例:不允许传入负数进行相加,若有负数则返回错误:

package main

import (
    "errors"
    "fmt"
)

func sum(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, errors.New("a 或者 b 不能是负数")
    }
    return a + b, nil
}

func main() {
    result, err := sum(1, 2)
    if err != nil {
        fmt.Println("发生错误:", err)
    } else {
        fmt.Println("计算结果:", result) // 输出: 计算结果: 3
    }

    // 若不需要某个返回值,可使用下划线 _ 占位忽略
    res, _ := sum(3, 4)
    fmt.Println(res)
}

1.3 命名返回参数

Go 语言支持在声明函数时直接为返回值命名。在函数体内部可以直接使用该变量,并在 return 时省略具体变量:

func sum(a, b int) (result int) {
    result = a + b
    return // 裸 return,默认返回命名参数 result 的最新值
}

1.4 可变参数

当函数的参数个数不固定时,可以使用可变参数语法 ...T(如 fmt.Println 就是典型的可变参数函数):

package main

import "fmt"

// params 被当作 []int 类型的切片处理
func sum1(params ...int) int {
    sum := 0
    for _, i := range params {
        sum += i
    }
    return sum
}

func main() {
    fmt.Println(sum1(1, 2, 3))       // 输出: 6
    fmt.Println(sum1(1, 2, 3, 4, 5)) // 输出: 15
}

1.5 包级可见性(Public 与 Private)

Go 语言不使用 publicprivate 关键字控制访问权限,而是依靠 函数名首字母的大小写 来修饰:

  • 首字母大写(如 PrintlnSum):公有函数,可被外部包导入后调用;
  • 首字母小写(如 sum):私有函数,仅限当前包内部调用。

1.6 匿名函数与闭包

匿名函数即没有名称的函数。在 Go 语言中,函数是一等公民(First-class Citizen),可以赋值给变量、作为参数传递或作为返回值返回。

1. 匿名函数赋值给变量

func main() {
    sum2 := func(a, b int) int {
        return a + b
    }
    fmt.Println(sum2(1, 2)) // 输出: 3
}

2. 闭包(Closure)

闭包是指一个函数有权访问其外部作用域中的变量。内部函数引用了外部函数的环境变量:

package main

import "fmt"

func closure() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {
    cl := closure()
    fmt.Println(cl()) // 输出: 1
    fmt.Println(cl()) // 输出: 2
    fmt.Println(cl()) // 输出: 3
}

在上述代码中,每次调用 cl() 时,变量 i 的状态都会被保存在闭包环境中并持续累加。


2. 方法

2.1 方法的定义与接收者

方法和函数极其相似,区别在于 方法在 func 关键字后必须指定一个接收者(Receiver),从而将该方法绑定到特定的类型上。

package main

import "fmt"

// 定义一个新类型 Age (基于 uint)
type Age uint

// 为 Age 类型绑定 String 方法 (值类型接收者)
func (age Age) String() {
    fmt.Println("the age is", age)
}

func main() {
    var age Age = Age(25)
    age.String() // 通过类型实例点操作符调用方法
}

2.2 值类型接收者 vs 指针类型接收者

方法的接收者分为 值类型接收者指针类型接收者

package main

import "fmt"

type Age uint

func (age Age) String() {
    fmt.Println("the age is", age)
}

// 指针类型接收者:可在方法内部修改接收者原本的值
func (age *Age) Modify() {
    *age = Age(30)
}

func main() {
    age := Age(25)
    age.String() // 输出: the age is 25

    age.Modify() // 修改 age 原本的值
    age.String() // 输出: the age is 30
}

自动转换语法糖

  • 使用 值变量 调用指针接收者方法时,Go 编译器会自动取地址((&age).Modify());
  • 使用 指针变量 调用值接收者方法时,Go 编译器会自动解引用((*agePointer).String())。

接收者选择原则

  1. 需修改接收者状态:选指针接收者;
  2. 结构体占用内存较大:为了避免拷贝全量数据,选指针接收者;
  3. 仅仅读取数据/小基础类型:选值接收者。

总结

在 Go 语言中:

  • 函数 从属于包(Package),主要用于独立的功能封装与代码复用;
  • 方法 关联到具体的数据类型(Type),代表该类型特有的行为特征。

思考题: 方法是否可以作为表达式赋值给一个变量?如果可以,如何通过该变量进行调用? 提示:Go 语言支持“方法表达式”(Method Expression)与“方法值”(Method Value),例如 f := age.String; f()