Go语言程序设计-第7章--接口

Go语言程序设计-第7章--接口

接口类型是对其他类型行为的概括与抽象。

Go 语言的接口的独特之处在于它是隐式实现。对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必须实现的方法即可。

7.1 接口即约定

7.2 接口类型

go 复制代码
package io

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

组合已有接口得到新的接口。

  • 嵌入式声明
go 复制代码
type ReadWriter interface {
	Reader
	Writer
}
  • 直接声明
go 复制代码
type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Close() error
}
  • 混合声明
go 复制代码
type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Writer
}

7.3 实现接口

接口的实现规则很简单,仅当一个表达式实现了一个接口时,这个表达式才可以赋值给接口。

go 复制代码
var w io.Writer
w = os.Stdout // OK: *os.File 有 Writable 方法
w = new(bytes.Buffer) // OK: * bytes.Buffer 有 Writable 方法
w = time.Second // 编译错误: time.Duration 没有 Writable 方法=

如果方法的接收者是一个指针类型,就不可以从一个无地址的对象上调用该方法,如:

go 复制代码
type IntSet struct {}
func (*IntSet) String() string

var _ = IntSet{}.String() // 编译错误:String 方法需要 *IntSet 接收者

可以从一个 IntSet 变量上调用此方法

go 复制代码
var s IntSet
var _ = s.String() // OK: s 是一个变量,&s 有 String 方法。

因为只有 *IntSet 有 String 方法,所以也只有 *IntSet 实现了 fmt.Stringer 接口。

go 复制代码
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // 编译错误: IntSet 缺少 String 方法。

可以把任何数据赋值给空接口类型。

go 复制代码
var any interface{} // 空接口类型
any = true
any = 12.34
any = "hello"
any = map[string]int {"one":1}
fmt.Println(any)

判断是否实现接口只需要比较具体类型和接口类型的方法,所以没有必要在具体类型的定义中声明这种关系。

非空的接口类型(如 io.Writer)通常由一个指针类型来实现,特别是当接口类型的一个或多个方法暗示会修改接收者的情形。一个指向结构的指针才是最常见的方法接收者。

7.4 使用 flat.Value 来解析参数

tempflag.go

go 复制代码
package main

import (
	"flag"
	"fmt"
)

type Celsius float64
type Fahrenheit float64

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9.0/5.0 + 32.0) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32.0) * 5.0 / 9.0) }

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

/*
//!+flagvalue
package flag

// Value is the interface to the value stored in a flag.
type Value interface {
	String() string
	Set(string) error
}
//!-flagvalue
*/

//!+celsiusFlag
// *celsiusFlag satisfies the flag.Value interface.
type celsiusFlag struct{ Celsius }

func (f *celsiusFlag) Set(s string) error {
	var unit string
	var value float64
	fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed
	switch unit {
	case "C", "°C":
		f.Celsius = Celsius(value)
		return nil
	case "F", "°F":
		f.Celsius = FToC(Fahrenheit(value))
		return nil
	}
	return fmt.Errorf("invalid temperature %q", s)
}

//!-celsiusFlag

//!+CelsiusFlag

// CelsiusFlag defines a Celsius flag with the specified name,
// default value, and usage, and returns the address of the flag variable.
// The flag argument must have a quantity and a unit, e.g., "100C".
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
	f := celsiusFlag{value}
	flag.CommandLine.Var(&f, name, usage)
	return &f.Celsius
}
var temp = CelsiusFlag("temp", 20.0, "the temperature")
func main() {
	flag.Parse()

	fmt.Println(*temp)
}

7.5 接口值

一个接口类型的值(简称接口值)其实有两个部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。

类型描述符来提供每个类型的具体信息,比如它的名字和方法。对于一个接口值,类型部分就用对应的类型描述符来表述。

如下四个语句中,变量 w 有三个不同的值(最初和最后是同一个值,都是 nil)

go 复制代码
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

每个语句后 w 的值和相关的动态行为。第一个语句声明了 w: var w io.Writer

在 Go 语言中,变量总是初始化为一个特定的值,接口也不例外。接口的零值就是把它的动态类型和值都设置为 nil

一个接口值是否是 nil 取决于它的动态类型,所以现在这是一个 nil 接口值。可以用 w == nil 或者 w != nil 来检测一个接口值是否是 nil

第二个语句把一个 *os.File 类型的值赋给了 w: w = os.Stdout

这次赋值把一个具体类型隐私转换为一个接口类型,它与对应的显式转换 io.Writer(os.Stdout) 等价。接口的动态类型会设置为指针类型 *os.File 的类型描述符,它的动态值会设置为 os.Stdout 的副本,即一个指向代表进程的标准输出的 os.File 类型的指针。

调用 w.Write([]byte("hello")) //

等价于 os.Stdout.Write([]byte("hello"))

接口值可以用 == 和 != 操作符来比较。

在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(如 slice),那么这个比较会导致宕机。

go 复制代码
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"

w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"

w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

注意:含有空指针的非空接口

一个接口值是否是 nil 取决于它的动态类型。

go 复制代码
const debug = true

func f(out io.Writer) {
	if out != nil {
		out.Write([]byte("done!\n"))
	}
}
func main() {
	var buf *bytes.Buffer
	if debug {
		buf = new(bytes.Buffer) // 启用输出收集
	}
	f(buf)
}

当 main 调用 f 时,把一个类型为*bytes.Buffer 的空指针赋给了 out 参数,所以 out 的动态类型是 *bytes.Buffer,动态值是 nil。但是判断 out != nil 判断的是它的动态类型,所以 out != nil 返回true。

调用 out.Write 时,不允许接收者为空。

正确方法:

go 复制代码
var buf io.Writer
if debug {
	buf = new(bytes.Buffer)
}

7.6 使用 sort.Interface 来排序

go 复制代码
package sort
type Interface interface {
	Len() int
	Less(i, j int) bool // i, j 是序列元素的下标
	Swap(i, j int)
}

要对序列排序,需要先确定一个实现了如上三个方法的类型,接着把 sort.Sort 函数应用到上面这类方法的实例上。

go 复制代码
type StringSlice []string
func (p StringSlice) Len() int { return len(p)}
func (p StringSlice) Less(i, j int) bool {return p[i] < p[j]}
func (p StringSlice) Swap(i, j int) {p[i], p[j] = p[j], p[i]}
go 复制代码
sort.Sort(StringSlice(names))

7.7 http.Handler 函数

go 复制代码
package http
type Handler interface {
	ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

net/http 包提供了一个请求多工转发器 ServeMux。

下面的代码中,将 /list, price 这样的 URL 和对应的处理程序关联起来。

go 复制代码
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	db := database{"shoes": 50, "socks": 5}
	mux := http.NewServeMux()
	//!+main
	mux.HandleFunc("/list", db.list)
	mux.HandleFunc("/price", db.price)
	//!-main
	log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

type database map[string]int

func (db database) list(w http.ResponseWriter, req *http.Request) {
	for item, price := range db {
		fmt.Fprintf(w, "%s: $%d\n", item, price)
	}
}

func (db database) price(w http.ResponseWriter, req *http.Request) {
	item := req.URL.Query().Get("item")
	if price, ok := db[item]; ok {
		fmt.Fprintf(w, "$%d\n", price)
	} else {
		w.WriteHeader(http.StatusNotFound) // 404
		fmt.Fprintf(w, "no such item: %q\n", item)
	}
}

7.8 error 接口

Error 是一个接口类型,包含返回错误消息的方法

go 复制代码
type error interface {
	Error() string
}

func New(text string) error { return &errorString{text}}

type errorString struct {text string}

func (e *errorString) Error() string { return e.Text }

直接调用 errors.New 比较罕见,一般使用 fmt.Errorf。

go 复制代码
package jmt

import "errors"

func Errorf(format string, args ...interface{}) error {
	return errors.New(Sprintf(format, args...))
}

7.10 断言类型

类型断言是一个作用在接口值上的操作,写出来类似 x.(T), 其中 x 是一个接口类型的表达式,而 T 是一个断言类型。类型断言会检查作为操作数的动态类型是否满足指定的断言类型。

go 复制代码
if f, ok := w.(*os.File); ok {
	// 使用 f
}

7.11 使用断言类型来识别错误

os.PathError

go 复制代码
type PathError struct {
	Op string
	Path string
	Err error
}

func (e *PathError) Error() string {
	return e.Op + " " + ": " + e.Err.Error()
}

可以根据类型断言来检查错误的特定类型,这些类型包含的细节远远多于一个简单的字符串。

go 复制代码
_, err := os.Open("/no/such/file")
fmt.Println(err)
fmt.Printf("%#v\n", err)

7.12 通过接口类型断言来查询特性

go 复制代码
func writeString(w io.Writer, s string)(n int, err error) {
	type stringWriter interface {
		WritesString(string) (n int, err error)
	}

	if sw, ok := w.(StringWriter); ok {
		return sw.WriteString(s) // 避免了内存复制
	}
	return w.Write([]byte(s))
}

7.13 类型分支

类型分支与普通的分支语句类似,差别是操作数改为 x.(type).

go 复制代码
switch x.(type) {
	case nil:  //
	case init, unit: //
	case bool: //
}
相关推荐
Dola_Pan3 分钟前
C语言:数组转换指针的时机
c语言·开发语言·算法
ExiFengs3 分钟前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
paj1234567895 分钟前
JDK1.8新增特性
java·开发语言
IT古董12 分钟前
【人工智能】Python在机器学习与人工智能中的应用
开发语言·人工智能·python·机器学习
繁依Fanyi16 分钟前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
湫ccc35 分钟前
《Python基础》之pip换国内镜像源
开发语言·python·pip
fhvyxyci36 分钟前
【C++之STL】摸清 string 的模拟实现(下)
开发语言·c++·string
qq_4597300339 分钟前
C 语言面向对象
c语言·开发语言
菜鸟学Python1 小时前
Python 数据分析核心库大全!
开发语言·python·数据挖掘·数据分析
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust