在〈结构入门〉中看过,有些数据会有相关性,相关联的数据组织在一起,对于数据本身的可用性或者是代码的可读性,都会有所帮助,实际上,有些数据与可处理它的函数也会有相关性,将相关联的数据与函数组织在一起,对数据与函数本身的可用性或者是代码的可读性,也有着极大的帮助。
创建方法
假设可能原本有如下的程序内容,负责银行帐户的创建、存款与提款:
package main
import (
"errors"
"fmt"
)
type Account struct {
id string
name string
balance float64
}
func Deposit(account *Account, amount float64) {
if amount <= 0 {
panic("必须存入正数")
}
account.balance += amount
}
func Withdraw(account *Account, amount float64) error {
if amount > account.balance {
return errors.New("余额不足")
}
account.balance -= amount
return nil
}
func String(account *Account) string {
return fmt.Sprintf("Account{%s %s %.2f}",
account.id, account.name, account.balance)
}
func main() {
account := &Account{"1234-5678", "Justin Lin", 1000}
Deposit(account, 500)
Withdraw(account, 200)
fmt.Println(String(account)) // Account{1234-5678 Justin Lin 1300.00}
}
实际上,Desposit
、Withdraw
、String
的函数操作,都是与传入的Account
实例有关,何不将它们组织在一起呢?这样比较容易使用些,在 Go 语言中,你可以重新修改函数如下:
package main
import (
"errors"
"fmt"
)
type Account struct {
id string
name string
balance float64
}
func (ac *Account) Deposit(amount float64) {
if amount <= 0 {
panic("必须存入正数")
}
ac.balance += amount
}
func (ac *Account) Withdraw(amount float64) error {
if amount > ac.balance {
return errors.New("余额不足")
}
ac.balance -= amount
return nil
}
func (ac *Account) String() string {
return fmt.Sprintf("Account{%s %s %.2f}",
ac.id, ac.name, ac.balance)
}
func main() {
account := &Account{"1234-5678", "Justin Lin", 1000}
account.Deposit(500)
account.Withdraw(200)
fmt.Println(account.String()) // Account{1234-5678 Justin Lin 1300.00}
}
简单来说,只是将函数的第一个参数,移至方法名之前成为函数调用的接收者(Receiver),这么一来,就可以使用account.Deposit(500)
、account.Withdraw(200)
、account.String()
这样的方式来调用函数,就像是面向对象程序语言中的方法(Method)。
注意到,在这边使用的是(ac *Account)
,也就是指针,如果你是如下使用(ac Account)
:
func (ac Account) Deposit(amount float64) {
if amount <= 0 {
panic("必须存入正数")
}
ac.balance += amount
}
那么执行像是account.Deposit(500)
,就像是以Deposit(*account, 500)
调用以下函数:
func Deposit(account Account, amount float64) {
if amount <= 0 {
panic("必须存入正数")
}
account.balance += amount
}
也就是,相当于将Account
实例以传值方式复制给Deposit
函数的参数。
某些程度上,可以将接收者想成是其他语言中的this
或self
,Go 建议为接收者适当命名,而不是用this
、self
之类的名称。接收者并没有文件上记载的作用,命名时不用其他参数具有一定的描述性,只要能表达程序意图就可以了,Go 建议是个一或两个字母的名称(某些程度上,也可以用来与其他参数区别)。
名称相同的方法
之前谈过,Go 语言中不允许方法重载(Overload),因此,对于以下的程序,是会发生String
重复定义的编译错误:
package main
import "fmt"
type Account struct {
id string
name string
balance float64
}
func String(account *Account) string {
return fmt.Sprintf("Account{%s %s %.2f}",
account.id, account.name, account.balance)
}
type Point struct {
x, y int
}
func String(point *Point) string { // String redeclared in this block 的编译错误
return fmt.Sprintf("Point{%d %d}", point.x, point.y)
}
func main() {
account := &Account{"1234-5678", "Justin Lin", 1000}
point := &Point{10, 20}
fmt.Println(account.String())
fmt.Println(point.String())
}
然而,若是将函数定义为方法,就不会有这个问题,Go 可以从方法的接收者辨别,该使用哪个String
方法:
package main
import "fmt"
type Account struct {
id string
name string
balance float64
}
func (ac *Account) String() string {
return fmt.Sprintf("Account{%s %s %.2f}",
ac.id, ac.name, ac.balance)
}
type Point struct {
x, y int
}
func (p *Point) String() string {
return fmt.Sprintf("Point{%d %d}", p.x, p.y)
}
func main() {
account := &Account{"1234-5678", "Justin Lin", 1000}
point := &Point{10, 20}
fmt.Println(account.String()) // Account{1234-5678 Justin Lin 1000.00}
fmt.Println(point.String()) // Point{10 20}
}
方法作为值
在 Go 语言中,函数也可以作为值传递,那么就产生了一个问题,方法呢?既然方法本质上也是个函数,那么是否也可以作为值传递,答案是可以的,不过,以上面的程序为例,你不能直接以String := String
这样的方式传递,而必须使用方法表达式(Method expression)。例如:
package main
import (
"errors"
"fmt"
)
type Account struct {
id string
name string
balance float64
}
func (ac *Account) Deposit(amount float64) {
if amount <= 0 {
panic("必须存入正数")
}
ac.balance += amount
}
func (ac *Account) Withdraw(amount float64) error {
if amount > ac.balance {
return errors.New("余额不足")
}
ac.balance -= amount
return nil
}
func (ac *Account) String() string {
return fmt.Sprintf("Account{%s %s %.2f}",
ac.id, ac.name, ac.balance)
}
func main() {
deposit := (*Account).Deposit
withdraw := (*Account).Withdraw
String := (*Account).String
account1 := &Account{"1234-5678", "Justin Lin", 1000}
deposit(account1, 500)
withdraw(account1, 200)
fmt.Println(String(account1)) // Account{1234-5678 Justin Lin 1300.00}
account2 := &Account{"5678-1234", "Monica Huang", 500}
deposit(account2, 250)
withdraw(account2, 150)
fmt.Println(String(account2)) // Account{5678-1234 Monica Huang 600.00}
}
可以看到,这样获取的函数,就像是本文一开始的范例那样,你可以传入任何的Account
实例。另一个获取方法的方式是方法值(Method value),这会保有获取方法当时的接收者:
package main
import (
"errors"
"fmt"
)
type Account struct {
id string
name string
balance float64
}
func (ac *Account) Deposit(amount float64) {
if amount <= 0 {
panic("必须存入正数")
}
ac.balance += amount
}
func (ac *Account) Withdraw(amount float64) error {
if amount > ac.balance {
return errors.New("余额不足")
}
ac.balance -= amount
return nil
}
func (ac *Account) String() string {
return fmt.Sprintf("Account{%s %s %.2f}",
ac.id, ac.name, ac.balance)
}
func main() {
account1 := &Account{"1234-5678", "Justin Lin", 1000}
acct1Deposit := account1.Deposit
acct1Withdraw := account1.Withdraw
acct1String := account1.String
acct1Deposit(500)
acct1Withdraw(200)
fmt.Println(acct1String()) // Account{1234-5678 Justin Lin 1300.00}
account2 := &Account{"5678-1234", "Monica Huang", 500}
acct2Deposit := account2.Deposit
acct2Withdraw := account2.Withdraw
acct2String := account2.String
acct2Deposit(250)
acct2Withdraw(150)
fmt.Println(acct2String()) // Account{5678-1234 Monica Huang 600.00}
}
值都能有方法
实际上,不只是结构的实例可以定义方法,在 Go 语言中,只要是值,就可以定义方法,条件是必须是定义的类型(defined type),具体而言,就是使用type
定义的新类型。
例如,以下的范例为[]int
定义了一个新的类型名称,并定义了一个ForEach
方法:
package main
import "fmt"
type IntList []int
type Funcint func(int)
func (lt IntList) ForEach(f Funcint) {
for _, ele := range lt {
f(ele)
}
}
func main() {
var lt IntList = []int{10, 20, 30, 40, 50}
lt.ForEach(func(ele int) {
fmt.Println(ele)
})
}
这个范例会显示 10 到 50 作为结果,必须留意的是,type
定义了新类型Funcint
,因为ForEach
是针对Funcint
定义,而不是针对[]int
,因此底下是行不通的:
lt2 := []int {10, 20, 30, 40, 50}
// lt2.ForEach undefined (type []int has no field or method ForEach)
lt2.ForEach(func(ele int) {
fmt.Println(ele)
})
编译器认为[]int
并没有定义ForEach
,因此发生错误,想要通过编译的话,可以进行类型转换:
lt2 := IntList([]int {10, 20, 30, 40, 50})
lt2.ForEach(func(ele int) {
fmt.Println(ele)
})
你甚至可以基于int
等基本类型定义方法,同样地,必须定义一个新的类型名称:
package main
import (
"fmt"
)
type Int int
type FuncInt func(Int)
func (n Int) Times(f FuncInt) {
if n < 0 {
panic("必须是正数")
}
var i Int
for i = 0; i < n; i++ {
f(i)
}
}
func main() {
var x Int = 10
x.Times(func(n Int) {
fmt.Println(n)
})
}
像这样基于某个基本类型定义新类型,并为其定义更多高阶特性,在 Go 的领域是常见的做法。这个范例会显示 0 到 9,看起来就像是指定函数,要求执行 x 次吧!…XD
nil 接收者
在 Go 中,接收者可以是nil
,这让你有机会在方法中处理接收者为nil
的情况,例如:
package main
import "fmt"
type Account struct {
id string
name string
balance float64
}
func (ac *Account) String() string {
if ac == nil {
return "<nil>"
}
return fmt.Sprintf("Account{%s %s %.2f}",
ac.id, ac.name, ac.balance)
}
func findById(id string) *Account {
accts := []*Account{&Account{"123", "Justin Lin", 10000}, &Account{"456", "Monica", 10000}}
for i := 0; i < len(accts); i++ {
if accts[i].id == id {
return accts[i]
}
}
return nil
}
func main() {
fmt.Println(findById("123").String())
fmt.Println(findById("789").String())
}
如果是其他语言,例如 Java 的话,在findById("789").String()
的地方会NullPointerException
,不过在 Go 中,可以针对接收者是否为nil
,来决定如何处理,例如这边就实现了 nil safety 的概念。
模拟构造函数、初始化化
Go 没有面向对象语言中构造函数或初始化化之类的概念,然而可以自行模拟,例如在container/list的源码可以看到New
作为一个工厂函数,用来创建新的List
,初始化的流程写在Init
方法之中:
...
// Init initializes or clears list l.
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New() *List { return new(List).Init() }