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

接受者参数

在上面,我们定义了一个「接受者参数」为结构体VertexAbs()方法,我们可以为任意类型的变量声明方法

这里我们使用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,类型*TM实现了这个接口,然后我们将接口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.Colorcolor.Model类型也是接口,但是通常因为直接使用预定义的实现image.RGBAimage.RGBAModel而被忽视了。这些接口和类型由image/color包定义。这里可以了解更多关于image包的信息。

posted @ 2019-11-29 16:48  Bylight  阅读(263)  评论(0编辑  收藏  举报