Skip to content

函数与接口

本文档详细介绍Go语言中的函数和接口,包括函数定义、方法、接口设计等核心概念和最佳实践。

📋 目录

函数基础

函数是Go语言的核心设计元素,用关键字func声明。Go的函数具有简洁而强大的特性。

函数定义语法

go
func functionName(parameter1 type1, parameter2 type2) (returnType1, returnType2) {
    // 函数体
    return value1, value2
}

基本函数示例

go
// 1. 无参数无返回值
func sayHello() {
    fmt.Println("Hello, Go!")
}

// 2. 有参数无返回值
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

// 3. 有参数有返回值
func add(a, b int) int {
    return a + b
}

// 4. 多个返回值
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// 5. 命名返回值
func calculate(a, b int) (sum, product int) {
    sum = a + b
    product = a * b
    return  // 自动返回命名的变量
}

函数调用示例

go
func main() {
    // 调用各种函数
    sayHello()
    greet("Alice")
    
    result := add(3, 4)
    fmt.Printf("3 + 4 = %d\n", result)
    
    quotient, err := divide(10, 3)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("10 / 3 = %.2f\n", quotient)
    }
    
    sum, product := calculate(5, 6)
    fmt.Printf("Sum: %d, Product: %d\n", sum, product)
}
go
package main
import "fmt"
// 返回a、b中最大值.
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}
func main() {
    x := 3
    y := 4
    z := 5
    max_xy := max(x, y) //调用函数max(x, y)
    max_xz := max(x, z) //调用函数max(x, z)
    fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
    fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
    fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它
}

上面这个里面可以看到max函数有两个参数,它们的类型都是int,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时注意到它的返回值就是一个类型,这个就是省略写法。

多个返回值

Go语言比C更先进的特性,其中一点就是函数能够返回多个值。

直接看例子

go
package main
import "fmt"
//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}
func main() {
    x := 3
    y := 4
    xPLUSy, xTIMESy := SumAndProduct(x, y)
    fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
    fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上面的例子可以看到直接返回了两个参数,当然也可以命名返回参数的变量,这个例子里面只是用了两个类型,也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。

go
func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}

变参

Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:

go
func myfunc(arg ...int) {}

arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个intslice

go
for _, n := range arg {
    fmt.Printf("And the number is: %d\n", n)
}

传值与传指针

传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

为了验证上面的说法,来看一个例子

go
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a int) int {
    a = a+1 // 改变了a的值
    return a //返回一个新值
}
func main() {
    x := 3
    fmt.Println("x = ", x)  // 应该输出 "x = 3"
    x1 := add1(x)  //调用add1(x)
    fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出"x = 3"
}

虽然调用了add1函数,并且在add1中执行a = a+1操作,但是上面例子中x变量的值没有发生变化

理由很简单:因为当调用add1的时候,add1接收的参数其实是x的copy,而不是x本身。

如果真的需要传这个x本身,该怎么办呢?

这就牵扯到了所谓的指针。变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子

go
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}
func main() {
    x := 3
    fmt.Println("x = ", x)  // 应该输出 "x = 3"
    x1 := add1(&x)  // 调用 add1(&x) 传x的地址
    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}

这样,就达到了修改x的目的。那么到底传指针有什么好处呢?

  • 传指针使得多个函数能操作同一个对象。

  • 传指针比较轻量级 (8bytes),只是传内存地址,可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当要传递大的结构体的时候,用指针是一个明智的选择。

  • Go语言中channelslicemap这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)

defer

Go语言中有种不错的设计,即延迟(defer)语句,可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当进行一些打开资源的操作时,遇到错误需要提前返回,在返回前需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,一般写打开一个资源是这样操作的:

go
func ReadWrite() bool {
    file.Open("file")
// 做一些工作
    if failureX {
        file.Close()
        return false
    }
    if failureY {
        file.Close()
        return false
    }
    file.Close()
    return true
}

上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在defer后指定的函数会在函数退出前调用。

go
func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}

如果有很多调用defer,那么defer是采用后进先出模式,所以如下代码会输出4 3 2 1 0

go
for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

通常来说,defer会用在释放数据库连接,关闭文件等需要在函数结束时处理的操作。

函数作为值、类型

在Go中函数也是一种变量,可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

go
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子

go
package main
import "fmt"
type testInt func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}
func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}
func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)    // 函数当做值来传递了
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)  // 函数当做值来传递了
    fmt.Println("Even elements of slice are: ", even)
}

函数当做值和类型在写一些通用接口的时候非常有用,通过上面例子看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是可以实现很多种的逻辑,这样使得程序变得非常的灵活。

Panic和Recover

Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panicrecover机制。一定要记住,应当把它作为最后的手段来使用,也就是说,代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。

Panic

是一个内建函数,可以中断原有的控制流程,进入一个panic状态中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panicgoroutine中所有调用的函数返回,此时程序退出。panic可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

Recover

是一个内建的函数,可以让进入panic状态的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入panic状态,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

下面这个函数演示了如何在过程中使用panic

go
var user = os.Getenv("USER")
func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

下面这个函数检查作为其参数的函数在执行时是否会产生panic

go
func throwsPanic(f func()) (b bool) {
    defer func() {
        if x := recover(); x != nil {
            b = true
        }
    }()
    f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
    return
}

注意:

defer必须在panic语句之前。

recover必须配合defer使用。

main函数和init函数

Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,强烈建议用户在一个package中每个文件只写一个init函数。

Go程序会自动调用init()main(),所以不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

import

在写Go代码的时候经常用到import这个命令用来导入包文件,经常看到的方式参考如下:

go
import(
    "fmt"
)

然后代码里面可以通过如下的方式调用

go
fmt.Println("hello world")

上面这个fmt是Go语言的标准库,其实是去GOROOT环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:

1、相对路径

go
import "./model" //当前文件同一目录的model目录,但是不建议这种方式来import

2、绝对路径

go
import "shorturl/model" //加载gopath/src/shorturl/model模块

上面展示了一些import常用的几种方式,但是还有一些

特殊的import

1、点操作

有时候会看到如下的方式导入包

go
import(
    . "fmt"
)

这个点操作的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名,也就是前面调用的fmt.Println("hello world")可以省略的写成Println("hello world")

2、别名操作

别名操作顾名思义可以把包命名成另一个用起来容易记忆的名字

go
import(
        f "fmt"
    )

别名操作的话调用包函数时前缀变成了前缀,即f.Println("hello world")

3、_操作

这个操作经常是让很多人费解的一个操作符,请看下面这个import

go
import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
)

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数

struct类型

struct类型的声明

Go语言中,也和C或者其他语言一样,可以声明新的类型,作为其它类型的属性或字段的容器。例如,可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型称之struct。如下代码所示:

go
type person struct {
    name string
    age int
}

声明一个struct如此简单,上面的类型包含有两个字段

  • 一个string类型的字段name,用来保存用户名称这个属性
  • 一个int类型的字段age,用来保存用户年龄这个属性

使用struct看下面的代码

go
type person struct {
    name string
    age int
}
var P person  // P现在就是person类型的变量了
P.name = "Astaxie"  // 赋值"Astaxie"给P的name属性.
P.age = 25  // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name)  // 访问P的name属性.

除了上面这种P的声明使用之外,还有另外几种声明使用方式:

  1. 按照顺序提供初始化值 P := person{"Tom", 25}

  2. 通过field:value的方式初始化,这样可以任意顺序 P := person{age:24, name:"Tom"}

  3. 当然也可以通过new函数分配一个指针,此处P的类型为*personP := new(person)

看一个完整的使用struct的例子

go
package main
import "fmt"
// 声明一个新的类型
type person struct {
    name string
    age int
}
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
// struct也是传值的
func Older(p1, p2 person) (person, int) {
    if p1.age>p2.age {  // 比较p1和p2这两个人的年龄
        return p1, p1.age-p2.age
    }
    return p2, p2.age-p1.age
}
func main() {
    var tom person
    // 赋值初始化
    tom.name, tom.age = "Tom", 18
    // 两个字段都写清楚的初始化
    bob := person{age:25, name:"Bob"}
    // 按照struct定义顺序初始化值
    paul := person{"Paul", 43}
    tb_Older, tb_diff := Older(tom, bob)
    tp_Older, tp_diff := Older(tom, paul)
    bp_Older, bp_diff := Older(bob, paul)
    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        tom.name, bob.name, tb_Older.name, tb_diff)
    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        tom.name, paul.name, tp_Older.name, tp_diff)
    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        bob.name, paul.name, bp_Older.name, bp_diff)
}

struct的匿名字段

定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct

看一个例子,让上面说的这些更具体化

go
package main
import "fmt"
type Human struct {
    name string
    age int
    weight int
}
type Student struct {
    Human  // 匿名字段,那么默认Student就包含了Human的所有字段
    speciality string
}
func main() {
    // 初始化一个学生
    mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
    // 访问相应的字段
    fmt.Println("His name is ", mark.name)
    fmt.Println("His age is ", mark.age)
    fmt.Println("His weight is ", mark.weight)
    fmt.Println("His speciality is ", mark.speciality)
    // 修改对应的备注信息
    mark.speciality = "AI"
    fmt.Println("Mark changed his speciality")
    fmt.Println("His speciality is ", mark.speciality)
    // 修改他的年龄信息
    fmt.Println("Mark become old")
    mark.age = 46
    fmt.Println("His age is", mark.age)
    // 修改他的体重信息
    fmt.Println("Mark is not an athlet anymore")
    mark.weight += 60
    fmt.Println("His weight is", mark.weight)
}

看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,匿名字段就是这样,能够实现字段的继承。student还能访问Human这个字段作为字段名。请看下面的代码。

go
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1

通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子

go
package main
import "fmt"
type Skills []string
type Human struct {
    name string
    age int
    weight int
}
type Student struct {
    Human  // 匿名字段,struct
    Skills // 匿名字段,自定义的类型string slice
    int    // 内置类型作为匿名字段
    speciality string
}
func main() {
    // 初始化学生Jane
    jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
    // 现在访问相应的字段
    fmt.Println("Her name is ", jane.name)
    fmt.Println("Her age is ", jane.age)
    fmt.Println("Her weight is ", jane.weight)
    fmt.Println("Her speciality is ", jane.speciality)
    // 修改他的skill技能字段
    jane.Skills = []string{"anatomy"}
    fmt.Println("Her skills are ", jane.Skills)
    fmt.Println("She acquired two new ones ")
    jane.Skills = append(jane.Skills, "physics", "golang")
    fmt.Println("Her skills now are ", jane.Skills)
    // 修改匿名内置类型字段
    jane.int = 3
    fmt.Println("Her preferred number is", jane.int)
}

从上面例子看出来struct不仅仅能够将struct作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。

这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?

Go里面很简单的解决了这个问题,最外层的优先访问,也就是当通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。

这样就允许去重载通过匿名字段继承的一些字段,当然如果想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子

go
package main
import "fmt"
type Human struct {
    name string
    age int
    phone string  // Human类型拥有的字段
}
type Employee struct {
    Human  // 匿名字段Human
    speciality string
    phone string  // 雇员的phone字段
}
func main() {
    Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
    fmt.Println("Bob's work phone is:", Bob.phone)
    // 如果要访问Human的phone字段
    fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}

method

函数的另一种形态,带有接收者的函数,称为method

method

现在假设有这么一个场景,定义了一个struct叫做长方形,现在想要计算他的面积,那么按照一般的思路应该会用下面的方式来实现

go
package main
import "fmt"
type Rectangle struct {
    width, height float64
}
func area(r Rectangle) float64 {
    return r.width*r.height
}
func main() {
    r1 := Rectangle{12, 2}
    r2 := Rectangle{9, 4}
    fmt.Println("Area of r1 is: ", area(r1))
    fmt.Println("Area of r2 is: ", area(r2))
}

这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。

这样实现当然没有问题,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,想计算他们的面积的时候怎么办?那就只能增加新的函数,但是函数名就必须要跟着换了,变成area_rectangle, area_circle, area_triangle...

椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。

很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。

基于上面的原因所以就有了method的概念,method是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。

用上面提到的形状的例子来说,method area() 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。

更具体地说,Rectangle存在字段 height 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。

用Rob Pike的话来说就是:

"A method is a function with an implicit first argument, called a receiver."

method的语法如下:

go
func (r ReceiverType) funcName(parameters) (results)

下面用最开始的例子用method来实现:

go
package main
import (
    "fmt"
    "math"
)
type Rectangle struct {
    width, height float64
}
type Circle struct {
    radius float64
}
func (r Rectangle) area() float64 {
    return r.width*r.height
}
func (c Circle) area() float64 {
    return c.radius * c.radius * math.Pi
}
func main() {
    r1 := Rectangle{12, 2}
    r2 := Rectangle{9, 4}
    c1 := Circle{10}
    c2 := Circle{25}
    fmt.Println("Area of r1 is: ", r1.area())
    fmt.Println("Area of r2 is: ", r2.area())
    fmt.Println("Area of c1 is: ", c1.area())
    fmt.Println("Area of c2 is: ", c2.area())
}

在使用method的时候重要注意几点

  • 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样

  • method里面可以访问接收者的字段

  • 调用method通过.访问,就像struct里面访问字段一样

在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。

值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。

那是不是method只能作用在struct上面呢?当然不是,他可以定义在任何自定义的类型、内置类型、struct等各种类型上面。什么叫自定义类型,自定义类型不就是struct,其实不是这样的,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。

type typeName typeLiteral

请看下面这个申明自定义类型的代码

go
type ages int
type money float32
type months map[string]int
m := months {
    "January":31,
    "February":28,
    ...
    "December":31,
}

这样就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int,回到method 可以在任何的自定义类型中定义任意多的method,接下来让看一个复杂一点的例子

go
package main
import "fmt"
const(
    WHITE = iota
    BLACK
    BLUE
    RED
    YELLOW
)
type Color byte
type Box struct {
    width, height, depth float64
    color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
    return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
    b.color = c
}
func (bl BoxList) BiggestColor() Color {
    v := 0.00
    k := Color(WHITE)
    for _, b := range bl {
        if bv := b.Volume(); bv > v {
            v = bv
            k = b.color
        }
    }
    return k
}
func (bl BoxList) PaintItBlack() {
    for i := range bl {
        bl[i].SetColor(BLACK)
    }
}
func (c Color) String() string {
    strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
    return strings[c]
}
func main() {
    boxes := BoxList {
        Box{4, 4, 4, RED},
        Box{10, 10, 1, YELLOW},
        Box{1, 1, 20, BLACK},
        Box{10, 10, 1, BLUE},
        Box{10, 30, 1, WHITE},
        Box{20, 20, 20, YELLOW},
    }
    fmt.Printf("We have %d boxes in our set\n", len(boxes))
    fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
    fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
    fmt.Println("The biggest one is", boxes.BiggestColor().String())
    fmt.Println("Let's paint them all black")
    boxes.PaintItBlack()
    fmt.Println("The color of the second one is", boxes[1].color.String())
    fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}

上面的代码通过const定义了一些常量,然后定义了一些自定义类型

  • Color作为byte的别名

  • 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性

  • 定义了一个slice:BoxList,含有Box

然后以上面的自定义类型为接收者定义了一些method

  • Volume()定义了接收者为Box,返回Box的容量

  • SetColor(c Color),把Box的颜色改为c

  • BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色

  • PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色

  • String()定义在Color上面,返回Color的具体颜色(字符串格式)

上面的代码通过文字描述出来之后是不是很简单?一般解决问题都是通过问题的描述,去写相应的代码实现。

指针作为receiver

现在让回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,可以使用*Box。

定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以需要传入指针。

这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解

这里也许会问SetColor函数里面应该这样定义*b.Color=c,而不是b.Color=c,需要读取到指针相应的值。

其实Go里面这两种方式都是正确的,当用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道要通过指针去获取这个值。PaintItBlack里面调用SetColor的时候是不是应该写成(&bl[i]).SetColor(BLACK),因为SetColor的receiver是*Box,而不是Box。这两种方式都可以,因为Go知道receiver是指针,他自动转了。

也就是说:

如果一个method的receiver是*T,可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method

类似的

如果一个method的receiver是T,可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method

所以不用担心是调用的指针的method还是不是指针的method,Go知道要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。

method继承

通过字段的继承的学习,发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。来看下面这个例子

go
package main
import "fmt"
type Human struct {
    name string
    age int
    phone string
}
type Student struct {
    Human //匿名字段
    school string
}
type Employee struct {
    Human //匿名字段
    company string
}
//在human上面定义了一个method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
    mark.SayHi()
    sam.SayHi()
}

method重写

上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子

go
package main
import "fmt"
type Human struct {
    name string
    age int
    phone string
}
type Student struct {
    Human //匿名字段
    school string
}
type Employee struct {
    Human //匿名字段
    company string
}
//Human定义method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
    mark.SayHi()
    sam.SayHi()
}

通过这些内容,可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。

interface

Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便

什么是interface

简单的说,interface是一组method签名的组合,通过interface来定义对象的一组行为。

前面例子中StudentEmployee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能say hi

继续做更多的扩展,StudentEmployee实现另一个方法Sing,然后Student实现方法BorrowMoneyEmployee实现SpendSalary

这样Student实现了三个方法:SayHiSingBorrowMoney;而Employee实现了SayHiSingSpendSalary

上面这些方法的组合称为interface(被对象StudentEmployee实现)。例如StudentEmployee都实现了interfaceSayHiSing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、SingBorrowMoney,因为Employee没有实现BorrowMoney这个方法。

interface类型

interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子

go
type Human struct {
    name string
    age int
    phone string
}
type Student struct {
    Human //匿名字段Human
    school string
    loan float32
}
type Employee struct {
    Human //匿名字段Human
    company string
    money float32
}
//Human对象实现Sayhi方法
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Human对象实现Sing方法
func (h *Human) Sing(lyrics string) {
    fmt.Println("La la, la la la, la la la la la...", lyrics)
}
//Human对象实现Guzzle方法
func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// Employee重载Human的Sayhi方法
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //此句可以分成多行
}
//Student实现BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount // (again and again and...)
}
//Employee实现SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount // More vodka please!!! Get me through the day!
}
// 定义interface
type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}
type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}
type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

通过上面的代码可以知道,interface可以被任意的对象实现。看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YoungChap两个interface。

最后,任意的类型都实现了空interface(这样定义:interface{}),也就是包含0个method的interface。

interface值

那么interface里面到底能存什么值呢?如果定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值。

因为m能够持有这三种类型的对象,所以可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和传统意义上面的slice有所不同。

来看一下下面这个例子:

go
package main
import "fmt"
type Human struct {
    name string
    age int
    phone string
}
type Student struct {
    Human //匿名字段
    school string
    loan float32
}
type Employee struct {
    Human //匿名字段
    company string
    money float32
}
//Human实现SayHi方法
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human实现Sing方法
func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}
//Employee重载Human的SayHi方法
func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone)
    }
// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
    SayHi()
    Sing(lyrics string)
}
func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
    //定义Men类型的变量i
    var i Men
    //i能存储Student
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")
    //i也能存储Employee
    i = tom
    fmt.Println("This is tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")
    //定义了slice Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //这三个都是不同类型的元素,但是他们实现了interface同一个接口
    x[0], x[1], x[2] = paul, sam, mike
    for _, value := range x{
        value.SayHi()
    }
}

通过上面的代码,发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子"。

空interface

空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

go
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊!

interface函数参数

interface的变量可以持有任意实现该interface类型的对象,这给编写函数(包括method)提供了一些额外的思考,是不是可以通过定义interface参数,让函数接受各种类型的参数。

举个例子:fmt.Println是常用的一个函数,是否注意到它可以接受任意类型的数据。打开fmt的源码文件,会看到这样一个定义:

go
type Stringer interface {
     String() string
}

也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,来试一试

go
package main
import (
    "fmt"
    "strconv"
)
type Human struct {
    name string
    age int
    phone string
}
// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
    return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
}
func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

现在再回顾一下前面的Box示例,发现Color结构也定义了一个method:String。其实这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。

go
//实现同样的功能
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。

interface变量存储的类型

interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

  • Comma-ok断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。

通过一个例子来更加深入的理解。

go
package main
import (
    "fmt"
    "strconv"
)
type Element interface{}
type List [] Element
type Person struct {
    name string
    age int
}
//定义了String方法,实现了fmt.Stringer
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
    list := make(List, 3)
    list[0] = 1 // an int
    list[1] = "Hello" // a string
    list[2] = Person{"Dennis", 70}
    for index, element := range list {
        if value, ok := element.(int); ok {
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
        } else {
            fmt.Printf("list[%d] is of a different type\n", index)
        }
    }
}

是否注意到了多个if里面,if里面允许初始化变量。断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的switch。

  • switch测试

重写上面的这个实现

go
package main
import (
    "fmt"
    "strconv"
)
type Element interface{}
type List [] Element
type Person struct {
    name string
    age int
}
//打印
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
    list := make(List, 3)
    list[0] = 1 //an int
    list[1] = "Hello" //a string
    list[2] = Person{"Dennis", 70}
    for index, element := range list{
        switch value := element.(type) {
            case int:
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
            case string:
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
            case Person:
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
            default:
            fmt.Println("list[%d] is of a different type", index)
        }
    }
}

这里有一点需要强调的是:element.(type)语法不能在switch外的任何逻辑里面使用,如果要在switch外面判断一个类型就使用comma-ok

嵌入interface

Go里面真正吸引人的是它内置的逻辑语法,就像在学习Struct时学习的匿名字段,那么相同的逻辑引入到interface里面,更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

可以看到源码包container/heap里面有这样的一个定义

go
type Interface interface {
    sort.Interface //嵌入字段sort.Interface
    Push(x interface{}) //a Push method to push elements into the heap
    Pop() interface{} //a Pop elements that pops elements from the heap
}

看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

go
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns whether the element with index i should sort
    // before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的ReaderWriter两个interface

go
// io.ReadWriter
type ReadWriter interface {
    Reader
    Writer
}

基于 MIT 许可发布