Go 语言入门(二)方法和接口
Go 语言入门(二)方法和接口
写在前面
在学习 Go 语言之前,我自己是有一定的 Java 和 C++ 基础的,这篇文章主要是基于A tour of Go编写的,主要是希望记录一下自己的学习历程,加深自己的理解
方法
Go 语言中是没有「类」这个概念的,但我们可以为变量定义方法,例如对结构体定义方法,达到类似于类的情况。这里我们先对 Go 中的方法进行一个定义:
什么是方法
「方法」:一类带特殊的接收者参数的函数
对于方法,「接受者参数」位于func
关键字和方法名
之间:
// 定义一个结构体
type Vertex struct {
X, Y float64
}
// 这里有一个接受者参数 v
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
// 我们可以直接调用 v 的 Abs() 方法
fmt.Println(v.Abs())
}
当然,我们也可以直接将接受者 v 作为一个参数传入,那么这就是一个普通的函数了,它们可以实现相同的功能:
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
接受者参数
在上面,我们定义了一个「接受者参数」为结构体Vertex
的Abs()
方法,我们可以为任意类型的变量声明方法。
这里我们使用type
关键字定义一个变量类型「MyFloat」:
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())
}
注意:只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。
指针接受者
对于其它语言有所了解的话,我们知道在函数实际上是对参数的拷贝进行操作;又由于「指针」的存在,有「实参」和「形参」之分。
在 Go 中同样有这两者的存在,对于某种类型 T:
-
如果接受者参数的类型为
T
,则是「形参」,函数中的修改不会修改原来的元素; -
如果接受者参数的类型为
*T
,则是「实参」,可以在函数中直接修改它指向的元素,这样能够同步修改原元素。
看下面的例子:
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}
// 对于方法,Go 可以自动在值和指针之间转换
// 因此这里等价于 (&v).Scale(10)
v.Scale(10)
fmt.Println(v.Abs())
}
当我们定义方法的时候,Go 语言会帮我们自动在值和指针之间转换;类似于上面,如果方法需求的是一个值,但我们传入的是指针,Go 也能够帮我们自动将其转换为值。但是使用函数时不会自动转换。
因此我们可以直接使用v.Scale(10)
,虽然传入的不是指针而是值,但这里的执行结果仍为 50;如果我们将Scale()
方法的接受者参数改为v Vertext
,那么Scale
函数中传入的是形参,只是对拷贝进行修改,运行后 v 的元素值不变,执行结果为 5。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用:当某一类型的所有方法接收者都是指针时,每个方法都会对变量本身进行修改;如果我们将值接收者和指针接收者混用,那在某一次调用指针接收者的方法后,对于后续的值接收者方法,可能会对本身值产生不应该的修改。
方法变量与表达式
Go 中我们可以将「使用方法」和「调用方法」两个操作分开。我们可以为一个方法变量赋值,让它成为一个函数,把方法绑定到特定接收者上,这样只需要提供参数而不需要提供接收者就可以调用:
拿上面的 Scale 方法做例子:
v := Vertex{3, 4}
scale := v.Scale
// 等价于 fmt.Println(v.Scale(10))
fmt.Println(scale(10))
有时,我们还希望能够灵活选择方法接收者,这时,我们可以将这个方法赋值为一个方法表达式。在调用方法表达式时,必须选择接收者,且将其作为第一个形参,之后则像原方法一样进行调用即可。
type Point struct{ X, Y float64 }
// Point 的两个方法
func (p Point) Add(q Point) { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) { return Point{p.X - q.X, p.Y - q.Y} }
func (path Path) TranslateBy(offset Point, add bool) {
// 方法表达式
var op func(p, q Point) Point
// 根据 add 值的不同为方法表达式 op 进行赋值
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
path[i] = op(path[i], offset)
}
}
接口
不同于 Java 中的接口,在 Go 中,接口是一种抽象类型。它就像一种「约定」,所有的接口类型都能引用其提供的所有方法。
「接口类型」:由一组方法签名定义的集合
type Abser interface {
Abs() float64
}
如何使用接口
如果一个类型实现了一个接口要求的所有方法,那么这个类型就实现了这个接口。
我们看下面代码的例子:
package main
import (
"fmt"
"math"
)
// 接口 Abser,包含方法 Abs()
type Abser interface {
Abs() float64
}
// MyFloat 实现了 Abs() 方法
type MyFloat float64
// 无需使用 implements 关键字
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
// *Vertex 实现了 Abs() 方法
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt(2))
v := Vertex{3, 4}
a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser
// 这里被注释的语句会报错
// v 是一个 Vertex(而不是 *Vertex),没有实现 Abser。
// a := v
fmt.Println(a.Abs())
fmt.Println(b.Abs())
}
上例代码中,类型MyFloat
和类型*Vertex
都实现了Abs()
方法,因此可以给接口类型Abser
赋值;而Vertex
未实现,如果用它来赋值则会报错。
同时,我们还可以从上面代码中看到,在实现Abser
接口的方法时,我们并没有像 Java 中实现接口一样用implements
关键字显式声明,这样也鼓励程序员对接口要有明确的定义。
接口值
「接口」作为一种抽象类型,它也是一个值,可以像其它值一样进行传递,也就是可以作为参数或是返回值。
下面我们定义一个接口I
,类型*T
和M
实现了这个接口,然后我们将接口I
作为参数传入函数desribe()
中:
package main
import (
"fmt"
"math"
)
type I interface {
M()
}
// 实现 I 的类型 *T
type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}
// 实现 I 的类型 F
type F float64
func (f F) M() {
fmt.Println(f)
}
func main() {
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)
}
运行后,输出如下:
(&{Hello}, *main.T)
Hello
(3.141592653589793, main.F)
3.141592653589793
可以看到,传入的接口参数会以底层的类型和值进行打印。
底层值为 nil 的接口值
如果接口的具体值为nil
,然后调用这个接口的方法,在一些语言中(如 Java)将会触发「空指针异常」,但在 Go 中则能够正确打印出 nil。
注意:保存了nil
具体值的接口自身并不为nil
。
我们使用前例的接口I
和类型*T
,作出测试如下:
1.「接口具体值」为 nil
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
}
执行结果如下:
(<nil>, *main.T)
<nil>
可以看到,在调用 i 的方法M()
时,Go 能够正常地打印出<nil>
值而不会报错。
2.「接口」自身为 nil
func main() {
var i I
describe(i)
i.M()
}
执行上面的语句,describe()
方法能够打印出(<nil>, <nil>)
,表示接口自身为nil
,自然值也为nil
;而在对接口 i 进行方法调用时则会抛出异常(因为 Go 不知道应该调用哪个具体方法的类型)。
空接口
「空接口」:定义了 0 个方法的接口
interface{}
空接口有什么作用呢?它能够接受任何类型的值(因为空接口对方法没有要求),因此我们可以用空接口来处理未知类型的值。例如,fmt.Print
可接受类型为interface{}
的任意数量的参数。
下面的例子可以有效地帮我们进行理解:
func main() {
var i interface{}
describe(i)
// 运行结果: (<nil>, <nil>)
i = 42
describe(i)
// 运行结果: (42, int)
i = "hello"
describe(i)
// 运行结果: (hello, string)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
上面的describe()
函数便以空接口作为参数,因此可以接受任何类型(包括 nil)的值。
类型断言
「类型断言」:提供了访问接口值底层具体值的方式。
t := i.(T)
该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。
若 i 并未保存 T 类型的值,该语句就会触发一个恐慌panic
。
为了判断一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
这个「双赋值」和映射中的很相似:
-
若 i 保存了一个 T,那么
t
将会是其底层值,而ok
为 true。 -
否则,
ok
将为false
而 t 将为 T 类型的零值,程序并不会产生恐慌。
下面我们用一个例子总结一下接口的「类型断言」:
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
// 执行结果: hello
s, ok := i.(string)
fmt.Println(s, ok)
// 执行结果: hello true
f, ok := i.(float64)
fmt.Println(f, ok)
// 执行结果: 0 false
f = i.(float64)
fmt.Println(f)
// 报错(panic)
}
类型选择
「类型选择」:一种按顺序从几个类型断言中选择分支的结构。
想象一下,如果我们需要根据变量的不同类型来对其进行特定操作,我们首先想到的便是在 if-else 中套用之前的类型断言:
if v, ok := i.(T); ok {
// v 的类型为 T
} else if v, ok := i.(S); ok {
// v 的类型为 s
} else {
// 没有匹配,v 与 i 的类型相同
}
Go 中套用swtich
语句,能够简单地针对给定接口值所存储的值的类型进行比较,也就是我们可以这样简化上面的代码:
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}
类型选择中的声明与类型断言i.(T)
的语法相同,只是具体类型T
被替换成了关键字type
。
此选择语句判断接口值 i 保存的值类型是 T 还是 S。
-
在 T 或 S 的情况下,变量 v 会分别按 T 或 S 类型保存 i 拥有的值。
-
在默认(即没有匹配)的情况下,变量 v 与 i 的接口类型和值相同。
注意:「类型选择」只对接口类型适用,我们不能对其他类型的值来使用。
嵌入interface
Go 里面真正吸引人的是它内置的逻辑语法,就像我们在学习 Struct 时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到 interface 里面,那不是更加完美了。如果一个 interface1 作为 interface2 的一个嵌入字段,那么 interface2 隐式的包含了 interface1 里面的 method。
我们可以看到源码包container/heap
里面有这样的一个定义:
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
给隐式的包含进来了。也就是下面三个方法:
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 包下面的 Reader 和 Writer 两个 interface:
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}
常用接口:Stringer
fmt
包中定义的Stringer
是最普遍的接口之一,它类似于 Java 中的toString()
方法,我们可以通过实现它来自定义在如何输出调用者。
fmt
包中具体定义如下:
type Stringer interface {
String() string
}
下面我们通过实现Stringer
接口,来自定义输出结构体Person
的值:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("Person: %v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a)
fmt.Println(z)
}
输出结果如下:
Person: Arthur Dent (42 years)
Person: Zaphod Beeblebrox (9001 years)
常用接口:error
Go 中用error
值来表示错误状态。与fmt.Stringer
类似,error
类型是一个内建接口:
type error interface {
Error() string
}
与fmt.Stringer
类似,fmt
包在打印错误值时也会满足我们实现(或者默认的)error
,因此我们可以通过实现Error()
方法来自定义需要打印的错误信息。
那么,如何获取是否产生了错误呢?Go 中用的还是熟悉的「双赋值」.
通常函数会返回一个error
值,调用的它的代码可以判断这个错误是否等于nil
来进行错误处理。
-
error
为 nil 时,表示执行成功,没有错误 -
error
不为 nil 时,表示执行失败,产生了错误
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
下面我们用一个例子来试试error
接口,我们实现一个开方的函数,并让传入参数为负数的时候产生错误:
package main
import (
"fmt"
"math"
)
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
// 注意:这里要 float64(e),不然会产生死循环
return fmt.Sprint(float64(e))
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
}
return math.Sqrt(x), nil
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
运行结果如下:
1.4142135623730951 <nil>
0 -2
这里需要解释一下为什么在Error()
方法中不能直接打印e
而要打印float64(e)
:
因为fmt
包在输出时也会试图匹配error
,e 变量通过实现Error()
的接口函数成为了error
类型,在fmt.Sprint(e)
时就会调用e.Error()
来输出错误的字符串信息,也就是下面的代码是等价的:
func (e MyError) Error() string {
return fmt.Printf(e)
}
// e 是一个 error,上面的语句实际上是这样的
// 这也就产生来死循环
func (e MyError) Error() string {
return fmt.Printf(e.Error())
}
常用接口:Reader
io
包指定了io.Reader
接口,它表示从数据流的末尾进行读取。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader
接口有一个Read()
方法:
func (T) Read(b []byte) (n int, err error)
Read()
方法做了两件事:
-
用数据填充给定的字节切片;
-
返回填充的「字节数」和「错误值」。
在遇到数据流的结尾时,它会返回一个io.EOF
错误。
示例代码创建了一个strings.Reader
并以每次 8 字节的速度读取它的输出:
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
}
}
}
执行结果如下:
// 第一次循环
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
// 第二次循环
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
// 第三次循环,遇到 EOF 异常,表示读取完毕,结束循环
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""
我们可以通过「A tour of Go」的在线练习来练习关于 Reader 接口的使用。
下面我们实现一个Reader
类型,它产生一个 ASCII 字符 'A' 的无限流。
package main
import "golang.org/x/tour/reader"
type MyReader struct{}
// TODO: 给 MyReader 添加一个 Read([]byte) (int, error) 方法
func (r MyReader) Read(b []byte) (int, error) {
// 1.填充字节切片
b[0] = 'A'
// 2.返回填充的字符数和错误值
return 1, nil
}
func main() {
reader.Validate(MyReader{})
}
常用接口:Image
image
包定义了Image
接口:
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x , y int) color.Color
}
注意: Bounds()
方法的返回值「Rectangle」实际上是一个image.Rectangle
,它在image
包中声明。
color.Color
和color.Model
类型也是接口,但是通常因为直接使用预定义的实现image.RGBA
和image.RGBAModel
而被忽视了。这些接口和类型由image/color
包定义。这里可以了解更多关于image
包的信息。