Go进阶之路——接口

接口 接口类型是一种抽象类型,是方法的集合,其他类型实现了这些方法就是实现了这个接口。
接口类型的值可以存放实现这些方法的任何值。

/* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] method_name3 [return_type] ... method_namen [return_type] }

简单示例:打印不同几何图形的面积和周长
package mainimport ( "fmt" "math" )type geometry interface { area() float32 perim() float32 }type rect struct { len, wid float32 }func (r rect) area() float32 { return r.len * r.wid }func (r rect) perim() float32 { return 2 * (r.len + r.wid) }type circle struct { radius float32 }func (c circle) area() float32 { return math.Pi * c.radius * c.radius }func (c circle) perim() float32 { return 2 * math.Pi * c.radius }func show(name string, param interface{}) { switch param.(type) { case geometry: // 类型断言 fmt.Printf("area of %v is %v \n", name, param.(geometry).area()) fmt.Printf("perim of %v is %v \n", name, param.(geometry).perim()) default: fmt.Println("wrong type!") } }func main() { rec := rect{ len: 1, wid: 2, } show("rect", rec)cir := circle{ radius: 1, } show("circle", cir)show("test", "test param") }

输出结果:
Go进阶之路——接口
文章图片

对于函数 show(name string, param interface{}),其传入的参数param可以是任意类型,只有实现了对应接口中的所有方法,才算是实现了该接口(即存在该类型为接收者的接口中对应的所有方法)。param.(type)获得接口类型名称。
package mainimport ( "fmt" "math" )type Abser interface { Abs() float64 }func main() { var a Abser f := MyFloat(-math.Sqrt2) v := Vertex{3, 4} a = f// a MyFloat 实现了 Abser fmt.Println(a.Abs()) a = &v // a *Vertex 实现了 Abser // 下面一行,v 是一个 Vertex(而不是 *Vertex) // 所以没有实现 Abser。 //a = v fmt.Println(a.Abs()) }type MyFloat float64func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) }type Vertex struct { X, Y float64 }func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) }

注意: 如上代码中, 由于 Vertex的 Abs 只定义在 *Vertex(指针类型) 上, 所以 Vertex(值类型) 不满足 `Abser`。
接口中可以内嵌接口(隐式接口)
【Go进阶之路——接口】类型通过实现那些方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implements“。
隐式接口解藕了实现接口的包和定义接口的包:互不依赖。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
对第一个例子做以下修改:
  • 首先添加 tmp 接口,该接口定义了 area() 方法
  • tmp 作为 geometry 接口中的匿名成员,并且将 geometry 接口中原本定义的 area() 方法删除
完成以上两步后,geometry 接口将会拥有 tmp 接口所定义的所有方法。运行结果和上述例子相同。
package mainimport ( "fmt" "math" )type tmp interface{ area() float32 }type geometry interface { // area() float32 tmp perim() float32 }type rect struct { len, wid float32 }func (r rect) area() float32 { return r.len * r.wid }func (r rect) perim() float32 { return 2 * (r.len + r.wid) }type circle struct { radius float32 }func (c circle) area() float32 { return math.Pi * c.radius * c.radius }func (c circle) perim() float32 { return 2 * math.Pi * c.radius }func show(name string, param interface{}) { switch param.(type) { case geometry: // 类型断言 fmt.Printf("area of %v is %v \n", name, param.(geometry).area()) fmt.Printf("perim of %v is %v \n", name, param.(geometry).perim()) default: fmt.Println("wrong type!") } }func main() { rec := rect{ len: 1, wid: 2, } show("rect", rec) cir := circle{ radius: 1, } show("circle", cir) show("test", "test param") }

Stringers 一个普遍存在的接口: fmt 包中定义的 Stringer
type Stringer interface { String() string }

Stringer 是一个可以用字符串描述自己的类型。`fmt`包 (还有许多其他包)使用这个来进行输出。
package mainimport "fmt"type Person struct { Name string Ageint }func main() { a := Person{"Arthur Dent", 42} z := Person{"Zaphod Beeblebrox", 9001} fmt.Println(a, z) }

输出结果:
Go进阶之路——接口
文章图片

实现Stringer接口
package mainimport "fmt"type Person struct { Name string Ageint }func (p Person) String() string { return fmt.Sprintf("%v (%v years)", p.Name, p.Age) }func main() { a := Person{"Arthur Dent", 42} z := Person{"Zaphod Beeblebrox", 9001} fmt.Println(a, z) }

输出结果:
Go进阶之路——接口
文章图片

如此实现了自定义的输出方式。
注:Sprintf 根据格式说明符设置格式,并返回结果字符串。
小练习:
IPAddr 类型实现 fmt.Stringer 以便用点分格式输出地址。
例如,`IPAddr{1,`2,`3,`4}` 应当输出 `"1.2.3.4"`。
package mainimport "fmt"type IPAddr [4]bytefunc (a IPAddr)String() string{ return fmt.Sprintf("%v.%v.%v.%v",a[0],a[1],a[2],a[3]) }func main() { addrs := map[string]IPAddr{ "loopback":{127, 0, 0, 1}, "googleDNS": {8, 8, 8, 8}, } for n, a := range addrs { fmt.Printf("%v: %v\n", n, a) } }

错误error Go 程序使用 error 值来表示错误状态。
fmt.Stringer 类似,`error` 类型是一个内建接口:
type error interface { Error() string }

(与 fmt.Stringer 类似,`fmt` 包在输出时也会试图匹配 `error`。)
package mainimport ( "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) } }

输出结果:
Go进阶之路——接口
文章图片

通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 `nil`, 来进行错误处理。
i, err := strconv.Atoi("42") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) } fmt.Println("Converted integer:", i)

error 为 nil 时表示成功;非 nil 的 error 表示错误。
注:strconv 包中结构体 NumError 实现了 error 接口,Atoi 函数将字符串转换为对应数字。
package mainimport ( "fmt" "strconv" )func main() { i, err := strconv.Atoi("42") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) } fmt.Println("Converted integer:", i) i, err = strconv.Atoi("i") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) } fmt.Println("Converted integer:", i) }

输出结果:
Go进阶之路——接口
文章图片

小练习:
从之前的练习中复制 Sqrt 函数,并修改使其返回 error 值。
Sqrt 接收到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。
创建一个新类型
type ErrNegativeSqrt float64

为其实现
func (e ErrNegativeSqrt) Error() string

使其成为一个 `error`, 该方法就可以让 ErrNegativeSqrt(-2).Error() 返回 `"cannot Sqrt negative number: -2"`。
注意: 在 Error 方法内调用 fmt.Sprint(e) 将会让程序陷入死循环。可以通过先转换 e 来避免这个问题:`fmt.Sprint(float64(e))`。
解释:
因为 ErrNegativeSqrt 实现了 error 接口,e也是error类型,故 fmt.Sprint(e)将调用e.Error()e转换为字符串。如果Error()方法调用fmt.Sprint(e),则程序将递归直到内存溢出。相当于:
func (e ErrNegativeSqrt) Error() string { return fmt.Sprint(e.Error()) }

故通过将e转换成一个非error类型(未实现Error接口)的值可以避免这种情况。
实际上在Error方法中把error值直接传递给fmt包中Print相关的函数都会导致无限循环。
看源码:print.go 中,Sprint 方法和 Sprintf 方法都用到了 printArg 方法,printArg 方法中用到了 handleMethods方法,该方法部分源码如下:
else { // If a string is acceptable according to the format, see if // the value satisfies one of the string-valued interfaces. // Println etc. set verb to %v, which is "stringable". switch verb { case 'v', 's', 'x', 'X', 'q': // Is it an error or Stringer? // The duplication in the bodies is necessary: // setting handled and deferring catchPanic // must happen before calling the method. switch v := p.arg.(type) { case error: handled = true defer p.catchPanic(p.arg, verb, "Error") p.fmtString(v.Error(), verb) returncase Stringer: handled = true defer p.catchPanic(p.arg, verb, "String") p.fmtString(v.String(), verb) return } } }

可见 p.fmtString(v.Error(), verb),如果传入的接口是error类型,则会调用其Error方法。

修改 Sqrt 函数,使其接受一个负数时,返回 ErrNegativeSqrt 值。
package mainimport ( "fmt" ) type ErrNegativeSqrt float64func (e ErrNegativeSqrt) Error() string{ return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e)) }func Sqrt(x float64) (float64, error) { if x<0 { return 0, ErrNegativeSqrt(x) } z := float64(1) for { temp := z - (z*z-x)/(2*z) if temp==z || (temp>z && temp-z<0.00000001) || (temp

输出结果:
Go进阶之路——接口
文章图片

Readers io 包指定了 io.Reader 接口, 它表示从数据流结尾读取。
Go 标准库包含了这个接口的许多实现, 包括文件、网络连接、压缩、加密等等。
io.Reader 接口有一个 Read 方法:
func (T) Read(b []byte) (n int, err error)

Read 用数据填充指定的字节 slice,并且返回填充的字节数和错误信息。 在遇到数据流结尾时,返回 io.EOF 错误。
例子代码创建了一个 strings.Reader。 并且以每次 8 字节的速度读取它的输出。
package mainimport ( "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进阶之路——接口
文章图片

源码如下:
func (r *Reader) Read(b []byte) (n int, err error) { if r.i >= int64(len(r.s)) { return 0, io.EOF } r.prevRune = -1 n = copy(b, r.s[r.i:]) r.i += int64(n) return }

Web 服务器 包 http 通过任何实现了 http.Handler 的值来响应 HTTP 请求:
package httptype Handler interface { ServeHTTP(w ResponseWriter, r *Request) }

在这个例子中,类型 Hello 实现了 `http.Handler`。
访问 http://localhost:4000/ 会看到来自程序的问候。
package mainimport ( "fmt" "log" "net/http" )type Hello struct{}func (h Hello) ServeHTTP( w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello!") }func main() { var h Hello err := http.ListenAndServe("localhost:4000", h) if err != nil { log.Fatal(err) } }

Go进阶之路——接口
文章图片

注:
Fprint 采用默认格式将其参数格式化并写入w。如果两个相邻的参数都不是字符串,会在它们的输出之间添加空格。返回写入的字节数和遇到的任何错误。
Fatal 等同于Print(),然后调用os.Exit(1)。

练习:HTTP 处理
实现下面的类型,并在其上定义 ServeHTTP 方法。在 web 服务器中注册它们来处理指定的路径。
type String stringtype Struct struct { Greeting string Punctstring Whostring }

例如,可以使用如下方式注册处理方法:
http.Handle("/string", String("I'm a frayed knot.")) http.Handle("/struct", &Struct{"Hello", ":", "Gophers!"})

package mainimport ( "fmt" "log" "net/http" )type String stringfunc (s String) ServeHTTP( w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, s) }type Struct struct { Greeting string Punctstring Whostring } func (s *Struct) ServeHTTP( w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, s.Greeting,s.Punct,s.Who) }func main() { http.Handle("/string", String("I'm a frayed knot.")) http.Handle("/struct", &Struct{"Hello", ":", "Gophers!"}) //http.Handle calls here log.Fatal(http.ListenAndServe("localhost:4000", nil)) }

Go进阶之路——接口
文章图片

Go进阶之路——接口
文章图片

图片 Package image 定义了 Image 接口:
package imagetype 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 包定义。
package mainimport ( "fmt" "image" )func main() { m := image.NewRGBA(image.Rect(0, 0, 100, 100)) fmt.Println(m.Bounds()) fmt.Println(m.At(0, 0).RGBA()) }


    推荐阅读