zoukankan      html  css  js  c++  java
  • 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)
    ", 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)
    ", 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
    ", 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, err, b)
            fmt.Printf("b[:n] = %q
    ", 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包的信息。

  • 相关阅读:
    jquery通过live绑定toggle事件
    svn is already locked解决方案
    不安全代码只会在使用 /unsafe 编译的情况下出现
    eclipse 新建 java 文件时自动生成注释
    浅谈权限设计
    ie6/IE8/IE9/谷歌以及火狐等浏览器下li 高度一致解决办法
    PHP+MSSQL TEXT字段被截断的解决方案
    利用CSS让dl dt dd呈现多行多列效果
    CSS实现图片水平垂直居中于DIV
    CSS 关于IE6 margin 为负数 负值的时候 正常显示的方法
  • 原文地址:https://www.cnblogs.com/Bylight/p/11955917.html
Copyright © 2011-2022 走看看