Go

Go语言的接口

Posted by BY on May 7, 2018

1.接口即约定

接口是一种抽象类型,不暴露所含数据的布局或者内部结构和基本操作,只提供一些方法而已。

例子:fmt.Printf和fmt.Sprintf都封装了fmt.Fprintf

func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

func Sprintf(format string, a ...interface{}) string {
	p := newPrinter()
	p.doPrintf(format, a)
	s := string(p.buf)
	p.free()
	return s
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

因为fmt.Fprintf仅依赖于io.Writer接口所约定的方法,对参数的具体类型没有要求,所以我们可以用任何满足io.Writer接口的具体类型作为fmt.Fprintf的实参。 这种,可以吧一种类型替换为满足同一接口的另一种类型的特征,称为可取代性

如下进行测试,Write仅仅统计传入数据的字节数。因为 *ByteCounter满足io.Writer的约定,所以可在Fprintf中使用,Fprintf并未察觉这种类型的差异,同时ByteCounter也能正确地鸡肋格式化后结果的长度。

import "fmt"

type ByteCounter int

func ( c *ByteCounter)Write(p []byte) (int, error)  {
	*c += ByteCounter(len(p))
	return len(p), nil
}

func main() {
	var c ByteCounter
	c.Write([]byte("hello"))
	fmt.Println(c)// 5

	c = 0
	var name  = "Dolly"
	fmt.Fprintf(&c, "hello, %s", name )
	fmt.Println(c) // 12
}

2.接口类型

要实现接口,必须实现接口类型定义中的所有方法

举例说,Reader抽象了所有可以读取字节的类型,Closer抽象了所有可以关闭的类型。

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

还可以通过组合已有接口,得到新的接口:

type ReadCloser interface {
	Reader
	Closer
}

type ReadWriteCloser interface {
	Reader
	Writer
	Closer
}

这种语法称为嵌入式接口。也可以不用嵌入式接口声明:

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

也可以混合:

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

3.实现接口

只有一个类型实现了这个接口要求的所有方法,这个类型才算实现了这个接口。

var rwc io.ReadWriteCloser
rwc = os.Stdout // *os.File有Read、Write、Close方法
rwc = new(bytes.Buffer) // 错误, *bytes.Buffer缺少Close方法

接口封装了对应的类型和数据,只有通过接口暴露的方法才可以调用,类型的其它方法则无法通过接口来调用:

func main() {
	os.Stdout.Write([]byte("hello")) // 正确,*os.File有Write方法
	os.Stdout.Close() // 正确, *os.File有Close方法

	var w io.Writer
	w = os.Stdout
	w.Write([]byte("hello")) // 正确,io.Writer有Write方法
	w.Close() // 错误,io.Writer没有Close方法
}

空接口类型的作用:对其实现类没有任何要求,可以把任何值赋给空接口类型:

func main() {
	var any interface{}
	any = true
	any = 12.34
	any = "hello"
	any = map[string]int{"one":1}
	any = new(bytes.Buffer)
}

像下面这种形式,就断言了*byte.Buffer类型的一个值必然实现了io.Writer:

var w io.Writer = new(byte.Buffer)

如果不想引用w而又想实现接口:

var _ io.Writer = (*byte.Buffer)(nil)

一个具体类型可以实现很多不相关的接口,那么它可以定义如下类型:

Album
Book
Movie
Magazine
Podcast
TVEposide
Track

可以把所有商品都具备的,用一种接口类型来表示,比如:标题,创建日期,创建者列表:

type Artifact interface {
	Title()    string
	Creators() []string
	Created()  time.Time
}

其他接口则限定于特定类型:

type Text interface {
	Pages()   int
	Words()   int // 字数属性只与书和杂志有关
	PageSize() int
}

type Audio interface {
	Stream()         (io.ReadCloser, error)
	RunningTime()    time.Duration
	Format()         string // 比如mp3..
}

type Video interface {
	Stream()         (io.ReadCloser, error)
	RunningTime()    time.Duration
	Format()         string // 比如mp4,mkv..
	Resolution()     (x, y int) // 分辨率
}

如果要把Audio和Video按照同样的方式来处理,就可以定义一个Streamer接口来呈现它们的共性,而不用修改现有的类型定义:

type Streamer interface {
	Stream()         (io.ReadCloser, error)
	RunningTime()    time.Duration
	Format()         string .
}

4.使用flag.Value解析参数

flag.Value帮助我们定义命令行标识

下面程序实现了睡眠指定时间的功能:

import (
	"time"
	"flag"
	"fmt"
)

var period = flag.Duration("period", 1*time.Second, "sleep period")

func main() {
	flag.Parse()
	fmt.Printf("Sleeping for %v...", *period)
	time.Sleep(*period)
	fmt.Println()
}

7.4-1

其中,flag.Duration 函数创建了一个time.Duration类型的标志变量,允许用户来指定时长。这种设计提供了良好的接口。

在flag包内,Value如下定义:

type Value interface {
	String() string
	Set(string) error
} 其中String方法用于格式化标志对应的值,可用于在命令行输出帮助信息。Set方法解析了传入的字符串参数并更新标志值。

下面定义celsiusFlag类型,允许在参数中使用摄氏或华氏温度。在celsiusFlag中内嵌的Celsius类型已经有String方法了,所以只需再定义一个Set方法。

import (
	"flag"
	"fmt"
)

type Fahrenheit float64
type Celsius float64

func CToF (c Celsius) Fahrenheit  { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius  { return Celsius((f-32)*5/9) }

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

type celsiusFlag struct{ Celsius }

func (f *celsiusFlag) Set(s string) error  {
	var uint string
	var value float64
	fmt.Sscanf(s, "%f%s", &value, &uint) // 无需检查错误
	switch uint {
	case "C", "`C":
		f.Celsius = Celsius(value)
		return nil
	case "F", "`F":
		f.Celsius = FToC(Fahrenheit(value))
		return nil
	}
	return fmt.Errorf("无效温度 %q", s)
}

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

调用Var方法会把*celsiusFlag的实参赋给flag.Value形参,编译器会检查 *celsiusFlag类型是否有flag.Value所必须的方法

7.4-2

5.使用sort.Interface来排序

在Go中,使用sort.Interface接口来指定通用排序算法每个具体的序列类型之间的协议。这个接口的实现确定了序列的具体布局和元素期望的排序方式。

一个原地排序算法需要三个信息:序列长度,比较两个元素,交换两个元素。所以sort.Interface接口有这三个方法:

type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less reports 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)
}

考虑字符串slice。

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

此时实现了Interface,可以进行简单的排序:

import (
	"sort"
	"fmt"
)

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

func main() {
	var a = []string{"a","c","z","f","e"}
	sort.Sort(StringSlice(a))
	fmt.Println(a) // [a c e f z]
}

另一个例子

下面 tracks是个播放列表:

import (
	"sort"
	"fmt"
	"time"
)

type Track struct {
	Titie      string
	Artist     string
	Album      string
	Year       int
	Length     time.Duration
}

var tracks = [] *Track{
	{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
	{"Go", "Moby", "Moby", 1992, length("3m37s")},
	{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
	{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}

func length (s string) time.Duration  {
	d, err := time.ParseDuration(s)
	if err != nil{
		panic(s)
	}
	return d
}

下面的printTracks将播放列表输出为一个表格。

func printTracks(tracks []*Track) {
	const format = "%v\t%v\t%v\t%v\t%v\t\n"
	tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
	fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length")
	fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------")
	for _, t := range tracks {
		fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
	}
	tw.Flush()
}

要按照Artist字段来对播放列表排序,需要先定义一个新的slice类型,以及必备的三个方法

type byArtist []*Track

func (x byArtist) Len() int           { return len(x) }
func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
func (x byArtist) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

测试:

func main() {
	fmt.Println("byArtist:")
	sort.Sort(byArtist(tracks))
	printTracks(tracks)
}

输出:

	byArtist:
	Title       Artist          Album              Year  Length
	-----       ------          -----              ----  ------
	Go Ahead    Alicia Keys     As I Am            2007  4m36s
	Go          Delilah         From the Roots Up  2012  3m38s
	Ready 2 Go  Martin Solveig  Smash              2011  4m24s
	Go          Moby            Moby               1992  3m37s
	

也可以“按照艺术家排序”,可以直接反序:

func main() {
	fmt.Println("\nReverse(byArtist):")
	sort.Sort(sort.Reverse(byArtist(tracks)))
	printTracks(tracks)
}

输出:

Reverse(byArtist):
Title       Artist          Album              Year  Length  
-----       ------          -----              ----  ------  
Go          Moby            Moby               1992  3m37s   
Ready 2 Go  Martin Solveig  Smash              2011  4m24s   
Go          Delilah         From the Roots Up  2012  3m38s   
Go Ahead    Alicia Keys     As I Am            2007  4m36s 

这个sort.Reverse:

type reverse struct {
	// This embedded Interface permits Reverse to use the methods of
	// another Interface implementation.
	Interface
}

// Less returns the opposite of the embedded implementation's Less method.
func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

// Reverse returns the reverse order for data.
func Reverse(data Interface) Interface {
	return &reverse{data}
}

可以定义byYear,按照其他类型进行排序:

type byYear []*Track

func (x byYear) Len() int  { return len(x) }
func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year }
func (x byYear) Swap (i, j int) { x[i], x[j] = x[j],x[i] }

测试:

func main() {
	sort.Sort(byYear(tracks))
	printTracks(tracks)
}

输出:

Title       Artist          Album              Year  Length  
-----       ------          -----              ----  ------  
Go          Moby            Moby               1992  3m37s   
Go Ahead    Alicia Keys     As I Am            2007  4m36s   
Ready 2 Go  Martin Solveig  Smash              2011  4m24s   
Go          Delilah         From the Roots Up  2012  3m38s 

上面每个Len和Swap都一样。

下面具体类型customSort组合了一个slice和一个函数,写一个比较函数可以定义一个新的排序。

type customSort struct {
	t      []*Track
	less   func(x, y *Track) bool
}
func (x customSort) Len() int  { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap (i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }

定义一个多层的比较函数,先按Title排序,然后是Year,最后是时长Length:

func main() {
	sort.Sort(customSort{tracks, func(x, y *Track) bool {
		if x.Title != y.Title{
			return x.Title < y.Title
		}
		if x.Year != y.Year{
			return x.Year < y.Year
		}
		if x.Length != y.Length {
			return x.Length < y.Length
		}
		return false
	}})
	printTracks(tracks)
}

输出:

Title       Artist          Album              Year  Length  
-----       ------          -----              ----  ------  
Go          Moby            Moby               1992  3m37s   
Go          Delilah         From the Roots Up  2012  3m38s   
Go Ahead    Alicia Keys     As I Am            2007  4m36s   
Ready 2 Go  Martin Solveig  Smash              2011  4m24s  

对长度为n的序列进行排序需要O(nlogn)次比较操作,而判断排好序只要比较n-1次,sort包提供的IsSorted函数可以做这个判断。与sort.Sort类似,它使用sort.Interface来抽象序列及其排序函数,只是从来都不会调用Swap方法。

func main() {
	values := []int{3, 1, 4, 1}
	fmt.Println(sort.IntsAreSorted(values)) // false
	sort.Ints(values)
	fmt.Println(values) // [1 1 3 4]
	fmt.Println(sort.IntsAreSorted(values)) // true
	sort.Sort(sort.Reverse(sort.IntSlice(values)))
	fmt.Println(values)// [4 3 1 1]
	fmt.Println(sort.IntsAreSorted(values)) // false
}

6.http.Handler 接口

先看一下源码:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

ListenAndServe需要一个服务器地址,以及一个Handler接口的实例。

下面map类型代表仓库,存储商品和价格的映射,加上一个ServeHTTP方法满足http.Handler接口。遍历map并且输出其中的元素(TGPL94):

func main() {
	db := database{"shoes": 50, "socks": 5}
	log.Fatal(http.ListenAndServe("localhost:8000", db))
}

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

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

另一个fetch程序(TGPL78):

func main() {
	for _, url := range os.Args[1:] {
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		b, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
			os.Exit(1)
		}
		fmt.Printf("%s", b)
	}
}

然后如图所示:

7.7-1

7.7-2

改进:上面这个只能列出所有商品并且不管URL。真实的服务器应该是定义不同的URL,每个触发不同的行为。

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request)  {
	switch req.URL.Path {
	case "/list" :
		for item, price := range db {
			fmt.Fprintf(w, "%s: %s\n", item, price)
		}
	case "/price":
		item := req.URL.Query().Get("item")
		price, ok := db[item]
		if !ok {
			w.WriteHeader(http.StatusNotFound)
			fmt.Fprintf(w, "no such item: %q\n", item)
			return
		}
		fmt.Fprintf(w, "%s\n", price)
	default:
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, "no such page: %s\n", req.URL)
		}
}

测试:

func main() {
	db := database{"shoes": 50, "socks": 5}
	log.Fatal(http.ListenAndServe("localhost:8000", db))
}

运行如图所示:

7.7-3

7.7-4


进一步:

真实的应用中,应当把每部分逻辑分到独立的函数或方法。某些相关的URL可能需要类似的逻辑,比如几个图片文件的URL可能都是/image/*.png形式。

Go中的net/http包提供了一个请求多工转发器ServeMux,用来简化URL和处理程序之间的关联。一个ServeMux把多个http.Handler组合成单个http.Handler。

下面代码,创建了一个ServeMux,将/list、/price这样的URL和对应的处理程序关联起来。最后主处理程序在ListenAndServe调用这个ServeMux:

func main() {
	db := database{"shoes": 50, "socks": 5}
	mux := http.NewServeMux()
	mux.Handle("/list", http.HandlerFunc(db.list))
	mux.Handle("/price", http.HandlerFunc(db.price))
	log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database)list (w http.ResponseWriter, req *http.Request)  {
	for item, price := range db{
		fmt.Fprintf(w, "%s: %s\n", item, price)
	}
}
func (db database) price (w http.ResponseWriter, req *http.Request)  {
	item := req.URL.Query().Get("item")
	price, ok := db[item]
	if !ok {
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, "no such item: %q\n", item)
		return
	}
	fmt.Fprintf(w, "%s\n", price)
}

这里的http.HandlerFunc(db.list)是一种类型转换,而不是函数调用。因为当调用db.list时,等价于以db为接收者调用database.list方法。所以说db.list是一个实现了处理功能的函数,而不是一个实例,因为它没有接口所需的方法,所以它不满足http.Handler接口,也不能传给mux.Handler

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

它就是一个让函数值满足接口的一个适配器,这种方式很常见,所以ServeMux引入了一种便捷方式:

mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)

同时net/http包提供一个全局的ServeMux实例DefaultServeMux,让DefaultServeMux作为服务器的主处理程序,无需传给ListenAndServe,直接传nil即可。

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

7.error接口

它只是一个接口类型,并不含一个返回错误消息的方法,源码如下:

type error interface{
    Error()  string
}

构造error最简单的方法是直接调用errors.New,完整的error包只有如下代码:

package errors

// New returns an error that formats as the given text.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}
  • 底层的errorString类型是一个结构,而没有直接用字符串,为了避免未来可能的布局变更。
  • 满足error接口的是*errorString指针,而不是原始的errorString,为了让每次New分配的error实例都不相等。避免重大错误与小错误相等
fmt.Println(errors.New("EOF") == errors.New("EOF")) // false

errors.New不常见。更常见的是fmt.Errorf,它额外提供了字符串格式化功能。

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

8.表达式求值器

将创建一个求值器用来计算算数表达式。

使用一个接口Expr,来代表这种语言中的任意一个表达式(暂时无方法)。

type Expr interface{}

定义五种类型,代表特定类型的表达式:

// Var表示一个变量
type Var string

// literal表示一个数字常量
type literal float64

//unary 表示一元操作符表达式:-y
type unary struct {
	op  rune
	x   Expr
}

//binary表示二元操作符表达式: x+y
type binary struct {
	op   rune
	x, y float64
}

// call 表示函数调用表达式, sin(x)
type call struct {
	fn   string
	args []Expr
}

要包含变量的表达式进行求值,需要一个数组映射:

type Env map[Var]float64 

因为每种类型的表达式都需要一个Eval方法来返回表达式在一个给定上下文下的值。可加入到接口中:

type Expr interface{
	Eval(env Env)   float64
}

分别实现Eval方法:

// Var中查询,有则返回值本身,无则返回0
func (v Var) Eval (env Env) float64  {
	return env[v]
}
// 返回的是数字常量
func (l literal) Eval (_ Env) float64{
	return float64(l)
}


func ( u unary) Eval(env Env) float64  {
	switch u.op {
	case '+':
		return +u.x.Eval(env)

	case '-':
		return -u.Eval(env)

	}
	panic(fmt.Sprintf("unsupported unary operator: %q", u.op))
}

func (b binary)Eval (env Env)float64  {
	switch b.op {
	case '+':
		return b.x.Eval(env) + b.y.Eval(env)
	case '-':
		return b.x.Eval(env) - b.y.Eval(env)
	case '*':
		return b.x.Eval(env) * b.y.Eval(env)
	case '/':
		return b.x.Eval(env) / b.y.Eval(env)
	}
	panic(fmt.Sprintf("unsupported binary operator: %q", b.op))
}

func (c call)Eval (env Env)float64  {
	switch c.fn {
	case "sin":
		return math.Sin(c.args[0].Eval(env))
	case "pow":
		return math.Pow(c.args[0].Eval(env),c.args[1].Eval(env))
	case "sqrt":
		return math.Sqrt(c.args[0].Eval(env))
	}
	panic(fmt.Sprintf("unsupported function call: %s", c.fn))
}

这样其实是不完整的,因为可能遇到解析不了的会报错。为了可以更快的发现错误,可以在接口中定义Check方法,用于在表达式语法树上检查静态错误。

type Expr interface{
	Eval(env Env)   float64
	Check(vars map[Var]bool)   error
}
// 返回空即可
func (v Var) Check (vars map[Var]bool) error  {
	vars[v] = true
	return nil
}

func (literal) Check (vars map[literal]bool) error {
	return nil
}

//先判断操作符是否合法,在递归检查操作数
func (u unary) Check (vars map[Var]bool) error {
	if !strings.ContainsRune("+-", u.op){
		return fmt.Errorf("unexpected unary op %q", u.op)
	}
	return u.x.Check(vars)
}

//先判断操作符是否合法,在递归检查操作数
func (b binary) Check(vars map[Var]bool) error {
	if !strings.ContainsRune("+-*/", b.op) {
		return fmt.Errorf("unexpected binary op %q", b.op)
	}
	if err := b.x.Check(vars); err != nil {
		return err
	}
	return b.y.Check(vars)
}

// 函数是否已知,参数个数是否正确,递归检查每个参数
func (c call) Check(vars map[Var]bool) error {
	arity, ok := numParams[c.fn]
	if !ok {
		return fmt.Errorf("unknown function %q", c.fn)
	}
	if len(c.args) != arity {
		return fmt.Errorf("call to %s has %d args, want %d",
			c.fn, len(c.args), arity)
	}
	for _, arg := range c.args {
		if err := arg.Check(vars); err != nil {
			return err
		}
	}
	return nil
}

var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}

9.类型断言

类型断言是一个作用在接口上的操作,基本形式x.(T),其中x是一个接口类型的表达式,T是一个断言类型

类型断言会检查作为操作数的动态类型是否满足断言类型。

  1. 断言类型是一个具体类型,则检查x的动态类型是否就是T。如果成功则取出动态值:
func main() {
	var w io.Writer
	w = os.Stdout
	f := w.(*os.File)
	c := w.(*bytes.Buffer)
	fmt.Println(f) // f == os.File
	fmt.Println(c) // 是*os.File, 不是*bytes.Buffer
}
// panic: interface conversion: io.Writer is *os.File, not *bytes.Buffer
  1. 断言类型T是一个接口类型,检查x的动态类型是否满足T。如果成功则仍然是一个接口值,只是接口的类型变为T。它只是接口类型的转换,但保留了接口值中的动态类型和动态值部分。
func main() {
	var w io.Writer
	w = os.Stdout
	rw1 := w.(io.ReadWriter)

	w = new(ByteCounter)
	rw2 := w.(io.ReadWriter)
	fmt.Println(rw1) // 成功
	fmt.Println(rw2) // 崩溃,*main.ByteCounter is not io.ReadWriter: missing method Read
}
type ByteCounter int

func (c *ByteCounter) Write(p []byte) (int, error) {
	*c += ByteCounter(len(p)) // convert int to ByteCounter
	return len(p), nil
}

10.使用类型断言来识别错误

在Go中,I/O会因为很多原因失败,os包中提供了三个帮助函数,对错误进行分类,包括文件已存储文件未找到权限不足

func IsExist(err error) bool {
	return isExist(err)
}

func IsNotExist(err error) bool {
	return isNotExist(err)
}

func IsPermission(err error) bool {
	return isPermission(err)
}

还有一种方式,是用专门的类型来表示结构化的错误值。在os包中定义了一个PathError类型来表示在与一个文件路径相关的操作上发生错误,记录了错误以及错误相关的操作和文件路径。

type PathError struct {
	Op   string
	Path string
	Err  error
}

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

关于以上中,PathError的Error方法只是拼接了所有的字段,而PathError的结构则保留了错误所有的底层信息。用法如下:

func main() {
	_, err := os.Open("/no/such/file")
	fmt.Println(err)
	fmt.Printf("%#v\n", err)
}

输出:

&os.PathError{Op:"open", Path:"/no/such/file", Err:0x3}

打印出了所有信息。

11.通过接口类型断言来查询特征

在网络的连接中,Write方法需要一个字节slice,而我们想写入的是一个字符串,所以[]byte(...)转换是必须的。这就涉及到了内存分配与复制,而内存分配会导致性能下降。

net/http中,可看到Write对应的动态类型还支持一个能搞笑写入字符串的WriteString方法,这个方法避免了临时内存的分配和复制。

可以假定一个新的接口,这个接口只包含WriteString方法,然后使用类型断言来判断w的动态类型是否满足这个新接口。

func writeString(w io.Writer, s string) (n int, err error){
	type stringWriter interface {
		WriteString(string) (n  int, err error)
	}
	if sw, ok := w.(stringWriter); ok {
		return sw.WriteString(s)  //  避免了内存复制
	}
	return w.Write([]byte(s))  //  分配了临时内存
}

一个具体的类型是否满足stringWriter接口仅仅由它拥有的方法决定,而不是这个类型与一个接口类型之间的一个关系声明。对于上面的例子来说,如果一个类型满足下面的接口,那么WriteString(s)必须与Write([]byte(s)等效。

interface {
		io.Writer
		WriteString(string) (n  int, err error)
	}

12.类型分支

接口中有两类不同的风格。

  • 第一种风格:接口中的各种方法突出了满足这个接口的具体类型之间的相似性,但隐藏了各个具体类型的布局和各自特有的功能,比如fmt.Stringersort.Interface等。
type Stringer interface {
	String() string
}

type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

  • 第二种风格:充分利用了接口值能够容纳各种具体类型的能力,把接口作为这个类型的联合(union)来使用。类型断言用来在运行时区分这些类型并分别处理。这种风格强调的是满足这个接口的具体类型,而不是这个接口的方法,不注重信息隐藏。这种风格的接口方式称为可识别联合

关于第二种风格,Go语言的数据库SQL查询API允许我们干净地分离查询中不变部分和可变部分。下面展示一个示例客户端:

func listTracks(db sql.DB, artist string, minYear, maxYear int)  {
	result, err := db.Exec(
		"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
		artist, minYear, maxYear)
	// ...
}

这个Exec方法把查询字符串中的每一个”?”都替换为与相应参数值对应的SQL字面量,这些参数可能是布尔型、数字、字符串或者nil。在Exec的实现代码中,可以发现一个类似如下的函数,将每个参数转为对应的SQL字面量。

func sqlQuote(x interface{}) string{
	if x == nil{
		return "NULL"
	} else if _,ok := x.(int); ok{
		return fmt.Sprintf("%d", x)
	} else if _, ok := x.(uint); ok {
		return fmt.Sprintf("%d", x)
	} else if b, ok := x.(bool); ok {
		if b {
			return "TRUE"
		}
		return "FALSE"
	} else if s, ok := x.(string); ok {
		return sqlQuoteString(s)
	} else {
		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
	}
}

也可以这么写:

switch x.(type){
case nil:     //   ...
case int, uint:     //   ...
case bool:     //   ...     //   ...     //   ...
case string:     //   ...     //   ...
default:     //   ...
}

要注意的是,分支的顺序在一个或多个分支是接口类型时会变得重要,因为可能有两个分支都能满足。其实类型分支语句也有一种扩展形式,它用来把每个分支中提取出来的原始值绑定到一个新的变量:

switch x := x.(type)  {/*....*/}

所以可以改写:

func sqlQuote(x interface{}) string{
	switch x := x.(type) {
	case nil:
		return "NULL"
	case int,uint:
		return fmt.Sprintf("%d", x) // 这里x的类型为interface
	case bool:
		if x {
			return "TRUE"
		}
		return "FALSE"
	case string:
		return sqlQuoteString(x)
	default:
		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
	}
}

每个单一类型的分支块内,变量x的类型都与该分支的类型一致。