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 是一个最典型的函数:
通用函数声明语法格式如下:
func关键字:用于声明函数;funcName函数名:遵循 Go 标识符命名规范;params入参列表:定义形参的变量名与类型,可以有零个或多个;result返回值类型:定义返回值的类型,可为空、单个或多个;body函数体:包含函数核心逻辑的大括号代码块。
定义一个计算两数之和的简单函数:
若连续多个形参类型相同,可以省略前面的类型声明:
在 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 时省略具体变量:
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 语言不使用 public 或 private 关键字控制访问权限,而是依靠 函数名首字母的大小写 来修饰:
- 首字母大写(如
Println、Sum):公有函数,可被外部包导入后调用; - 首字母小写(如
sum):私有函数,仅限当前包内部调用。
1.6 匿名函数与闭包¶
匿名函数即没有名称的函数。在 Go 语言中,函数是一等公民(First-class Citizen),可以赋值给变量、作为参数传递或作为返回值返回。
1. 匿名函数赋值给变量¶
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())。
接收者选择原则¶
- 需修改接收者状态:选指针接收者;
- 结构体占用内存较大:为了避免拷贝全量数据,选指针接收者;
- 仅仅读取数据/小基础类型:选值接收者。
总结¶
在 Go 语言中:
- 函数 从属于包(Package),主要用于独立的功能封装与代码复用;
- 方法 关联到具体的数据类型(Type),代表该类型特有的行为特征。
思考题: 方法是否可以作为表达式赋值给一个变量?如果可以,如何通过该变量进行调用? 提示:Go 语言支持“方法表达式”(Method Expression)与“方法值”(Method Value),例如
f := age.String; f()。