Go语言方法和接口基础

概述

几种当前较流行的编程语言在对面向对象编程的设计上都大同小异,Go也不例外,如果理解了面向对象的理念学习起来还是很简单的。

方法

概述

Go语言中没有其他常见的语言中的的概念,它通过其他方法来实现面向对象这一设计。

定义

方法是一类带特殊接收者参数的函数,这个参数写在func关键字和方法名之间。

我们可以为一个结构体类型定义方法:

示例1:method.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

示例中Abs()方法拥有一个Vertex类型的接收者v,或者也可以说Vertex类型的结构体v拥有一个Abs()方法(函数)。

注意

方法即是函数,但他的写法同一般函数不同,下面的这种写法Abs()就只是个正常的函数

1
2
3
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

两者的差别就在于是否带有接收者参数

上面的例子中,方法的接收者是一个结构体,我们也可以使用其他非结构体类型,它们同样可以声明方法。

不过你只能为在同一个包内定义的类型的接收者声明方法,而不能为其他包内定义的类型的接收者声明方法,也就意味着你不能对Go内建的类型(int、float等)声明方法。(因为这些类型在其他包内定义),下面这个例子我们为一个MyFloat类型的接收者声明了Abs()方法:

示例2:method-continue.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}

可以看到我们虽然不能直接对一个float64类型的接收者声明方法,但是实际上Myfloat与float64是等价的,通过type关键字的定义,Myfloat是一种新的类型,但仍然具有float64类型的特性。

指针类型同样可以声明方法,并且不必像示例2中写的那样重新定义一个新的类型,对于一个类型T,在接收者参数中直接使用*T可以为它声明方法。

一个指针接收者的方法可以修改接收者指向的值,这种方法要比使用值接收者更常用

示例3:method-pointers.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}

Scale方法的接收者是一个指针,调用这个方法可以修改这个指针所指向的值,也就是我们定义的Vertex类型变量v,我们期望的结果应该是50。

编译运行结果为50,符合我们的期望。

如果将Scale方法的*去掉,则运行结果为5,很明显程序并没有按我们的期望运行。

为什么会出现这种情况?

将*去掉后Scale方法操作的值不再是变量v本身,而是v的一个副本,我们在main函数一开始声明的v的值并没有受影响。


方法与指针重定向

当以指针为接收者的方法在被调用时,接收者既可以是值也可以是指针:

1
2
3
4
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK

两种形式均可以通过编译

反过来同样成立,在以值为接收者的方法被调用时,接收者同样既可以是值也可以是指针


上面说过使用指针接收者的方法更加常用,这么做的原因有二:

  1. 指针接收者的方法能够修改其接收者指向的值
  2. 可以避免在每次调用方法是复制该值。在值的类型是大型结构体的时候这么做会更加高效

接口

概述

接口是一个封装好的代码块所暴露给外部的方法,用户或者其他程序可以通过这个接口调用其中的代码来实现相应的功能而无需关心这段代码的具体形式。

定义

在Go中接口是一种类型,是由一个或一组方法签名定义的集合,我们可以初始化一个接口类型的变量,这个变量可以保存任何实现了接口中的方法的值

1
2
3
4
5
6
type I interface {
M1() int
M2() float64
...
Mn() string
}

一个类型如果实现了一个接口的所有方法,我们称这歌类型实现了该接口,无需专门显式声明。

示例4:interface.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type I interface {
M()
}

type T struct {
S string
}

// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
fmt.Println(t.S)
}

func main() {
var i I = T{"hello"}
i.M()
}

编译运行

1
hello

示例中我们在main函数中声明并初始化了一个变量i,它的类型是I,也就是接口类型,它的值为T{“hello”}。

可以看到接口也是值,那么它便同样可以被传递,用作函数的参数或是函数的返回值

示例5:interface-values.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"math"
)

//声明接口
type I interface {
M()
}

//声明类型
type T struct {
S string
}

//实现接口(指针)
func (t *T) M() {
fmt.Println(t.S)
}

//声明类型
type F float64

//实现接口(值)
func (f F) M() {
fmt.Println(f)
}

func main() {
//声明接口值i
var i I

i = &T{"Hello"}
describe(i)
i.M()

i = F(math.Pi)
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

可以看到describe函数接受一个接口类型的参数,并输出接口值

编译运行

1
2
3
4
(&{Hello}, *main.T)
Hello
(3.141592653589793, main.F)
3.141592653589793

两条结果分别是*T类型接口实现和F类型接口实现,不难看出在内部接口值被看作一个包含值和具体类型的元组。

接口值保存了一个具体的底层类型的具体值

接口值在调用方法时会执行其底层类型的同名方法

多个类型可以实现同名接口,一个接口在面对不同类型的值的时候会执行相应的方法,这也就是面向对象中的多态的概念,即代码可以根据类型采取不同的行为


面对nil值的情况

nil接口值既不保存值也不保存类型,所以对一个nil接口调用方法时编译器会报错,因为这个接口内未包含能够指命该调用哪个具体方法的类型

示例6:nil-inerface-values.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type I interface {
M()
}

func main() {
var i I
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf("(%v,%T)\n",i,i)
}

编译报错

1
2
(<nil>,<nil>)
$panic: runtime error: invalid memory address or nil pointer dereference

可以看到由于接口i的底层类型为nil,编译器无法找到对应的具体方法的类型,所以报出错误:无效的地址或无法解引用nil指针

如果接口的具体值为nil会如何

示例7:interface-values-with-nil.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

type I interface {
M()
}

type T struct {
S string
}

func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}

func main() {
var i I

var t *T
i = t
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

这次接口i指向了一个底层值t,t是一个指针,值为nil,再次编译

1
2
(<nil>,*main.T)
<nil>

注意

保存了nil具体值的接口其自身并不为nil


定义了零个方法的接口被称为空接口,空接口可以保存任何类型的值(因为任何类型都至少实现了零个方法)

1
var i interface {}

通常我们使用空接口来处理未知类型的值

Go语言提供了访问接口值底层具体值的方式,使用

1
t := i.(T)

对一个接口值进行类型断言,该语句断言接口值i保存了具体类型T,并将其底层类型为T的值赋值给t

判断一个接口值是否保存了一个特性类型可以使用

1
t,ok := i.(T)

这时类型断言会返回两个值:一个底层值和一个报告断言是否成功的布尔值,如果i保存了一个T,则t为其底层值,ok为true,反之则为t的值为T类型的零值,ok为false

如果i并没有保存T类型的值,直接使用第一种语句会触发一个恐慌,而使用第二种则不会

示例8:type-assertions.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
var i interface{} = "hello"

s := i.(string)
fmt.Println(s)

s, ok := i.(string)
fmt.Println(s, ok)

f, ok := i.(float64)
fmt.Println(f, ok)

f = i.(float64) // 报错(panic)
fmt.Println(f)
}

编译运行,我们可以看到编译器因为接口保存的类型与断言类型不符而触发了panic:

1
2
3
4
hello
hello true
0 false
$panic: interface conversion: interface {} is string, not float64

当需要多个类型断言时,我们可以使用类型选择,这是一种按顺序从几个类型断言中选择分支的结构,类型选择的形式与switch语句类似

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T: //v的类型为T时
...
case S: //v的类型为S时
...
default: // 没有匹配,v的类型与i的类型相同
...
}

类型选择语句可以针对给定接口值所存储的值的类型进行比较,选择符合的类型

类型选择中的声明与类型断言中的i.(T)的语法相同,只不过把T替换成了关键字type

示例9:type-switch.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

func main() {
do(21)
do("hello")
do(true)
}

编译运行

1
2
3
Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!

可以看到v在匹配到类型的条件下值为相应类型的值,在未匹配的条件下v与i的接口类型和值相同

练习

通过让IPAddr类型实现fmt.Stringer来打印点号分隔的地址

例如:IPAddr{1,2,3,4}应当打印为“1.2.3.4”


Stringer接口

fmt包中定义的Stringer接口是最普遍的接口之一

1
2
3
type Stringer interface {
String() string
}

Stringer是一个可以用字符串描述自己的类型。fmt包中都通过此接口来打印值


练习1:exercise-stringer.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type IPAddr [4]byte

//TODO: 给 IPAddr 添加一个 "String() string" 方法
func (ipaddr IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v",ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3])
}

func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}

编译运行

1
2
loopback: 127.0.0.1
googleDNS: 8.8.8.8

错误

概述

在程序运行中如果发生了错误,我们需要事先约定返回一个错误代码以供我们分析调试,这时就需要错误处理机制,不然我们只能一级级上报直到一个能够处理这个错误的函数

常见语言通常都内置了一套错误处理机制,Go语言也不例外

定义

Go语言使用error值来表示错误状态,它是一个内建的接口

1
2
3
type error interface {
Error() string
}

通常情况下函数会返回一个error值,调用它的代码应当判断这个错误是否等于nil来进行错误处理

error为nil时表示成功,反之为失败

示例10:errors.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"time"
)

type MyError struct {
When time.Time
What string
}

func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}

func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}

func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}

编译运行

1
at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work

练习

  1. 之前的练习中复制Sqrt函数,修改他使其返回error值。
  • Sqrt函数接受到一个负数时,应当返回一个非nil的错误值,同样复数也也不被支持

  • 创建一个新的类型

    1
    type ErrNegativeSqrt float64

    并为其实现

    1
    func (e ErrNegativeSqrt) Error() string {}

    方法使其拥有error值,通过ErrNegativeSqrt(-2).Error调用该方法应返回“cannot Sqrt negative number: -2”

    注意

    在Error方法内调用fmt.Sprint(e)会让程序陷入死循环。

    我们可以通过先转换e来避免这个问题:fmt.Sprint(float64(e))

  • 修改Sqrt函数,使其接受一个负数时,返回ErrNegativeSqrt值

练习2:exercise-errors.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
if e < 0 {
return fmt.Sprint("cannot Sqrt negative number: ", float64(e))
} else {
return ""
}
}

func Sqrt(x float64) (float64, error) {
z := float64(1)
err := ErrNegativeSqrt(x)
if x < 0 {
return 0, err
} else {
for i := 0; i < 10; i++ {
z -= (z*z - x) / (2 * z)
}
return z, err
}
}

func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}

编译运行

1
2
1.414213562373095 
0 cannot Sqrt negative number: -2
  1. 实现一个Reader类型,它产生一个ASCII字符‘A’的无限流

Reader接口

io包指定了io.Reader接口,它表示从数据流的末尾进行读取

io.Reader接口有一个Read方法

1
func (T) Read(b []byte) (n int, err error)

Read用数据填充给定的字节切片并返回填充的字节数和错误值,在遇到数据流结尾时,他会返回一个io.EOF错误

下面的示例代码创建了一个strings.Reader并以每次8字节的速度读取它的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}

Go的标准库中包含了许多该接口的实现,包括文件、网络连接、压缩和加密等


练习3:exercise-reader.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "golang.org/x/tour/reader"

type MyReader struct{}

// TODO: 给 MyReader 添加一个 Read([]byte) (int, error) 方法
func (r MyReader) Read(b []byte) (n int, err error) {
for i:= range b {
b[i] = 65
n = len(b)
}
return

}

func main() {
reader.Validate(MyReader{})
}
  1. 编写一个实现了io.Reader并从另一个io.Reader中读取数据的rot13Reader,通过应用rot13代换密码对数据流进行修改。

练习4:exercise-rot-reader.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"io"
"os"
"strings"
)

type rot13Reader struct {
r io.Reader
}

func (r rot13Reader) Read(b []byte) (int, error) {
n, err := r.r.Read(b)
for i, v := range b {
switch true {
case v >= 65 && v < 78:
b[i] = v + 13
case v >= 77 && v < 91:
b[i] = v - 13
case v >= 97 && v < 110:
b[i] = v + 13
case v >= 109 && v < 123:
b[i] = v - 13
default:
b[i] = v
}
}
return n, err
}

func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}

编译运行

1
You cracked the code!
  1. 定义一个Image类型,实现必要的方法并调用pic.ShowImage,使其返回一个image.Image的实现而非切片。
  • Bounds应当返回一个image.Rectangle,例如image.Rect(0,0,w,h)
  • ColorModel应当返回color.RGBAModel
  • At应当返回一个颜色。之前的图片生成器的值v对应于此次的color.RGBA{v,v,255,255}

在具体实现之前先看下Go内置的image包对Image接口的定义

1
2
3
4
5
6
7
package image

type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x,y int) color.Color
}

注意

Bounds方法的返回值Rectangle实际上是一个image.Rectangle,他在image包中声明

在Go的文档中可以查到详细的信息


练习5:exercise-images.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"golang.org/x/tour/pic"
"image"
"image/color"
)

type Image struct{}

func (i Image) Bounds() image.Rectangle {
return image.Rect(0,0,200,200)
}

func (i Image) ColorModel() color.Model {
return color.RGBAModel
}

func (i Image) At(x, y int) color.Color {
return color.RGBA{uint8(x), uint8(y), uint8(255), uint8(255)}
}

func main() {
m := Image{}
pic.ShowImage(m)
}

编译运行

result

作者

Luc_41

发布于

2020-03-23

更新于

2020-03-29

许可协议

评论