结构与方法


在〈结构入门〉中看过,有些数据会有相关性,相关联的数据组织在一起,对于数据本身的可用性或者是代码的可读性,都会有所帮助,实际上,有些数据与可处理它的函数也会有相关性,将相关联的数据与函数组织在一起,对数据与函数本身的可用性或者是代码的可读性,也有着极大的帮助。

创建方法

假设可能原本有如下的程序内容,负责银行帐户的创建、存款与提款:

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}
}

实际上,DespositWithdrawString的函数操作,都是与传入的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函数的参数。

某些程度上,可以将接收者想成是其他语言中的thisselfGo 建议为接收者适当命名,而不是用thisself之类的名称。接收者并没有文件上记载的作用,命名时不用其他参数具有一定的描述性,只要能表达程序意图就可以了,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() }




展开阅读全文