《Go语言圣经》第七章 接口

  • 接口类型是对其它类型行为的概括和抽象。通过使用接口,可以写出更灵活的函数,这些函数不用绑定在一个特定的类型实现上。
  • Go 语言的接口的独特之处在于它是隐式实现。

7.1、接口即约定

  • 接口是一种抽象类型,它不会暴露(对象)所含数据的布局或者内部结构,也没有数据的基本操作,它所提供的只是一些方法。
  • io.Writer 接口类型的声明:
package io
// Writer 接口封装了基础的写入方法
type Writer interface {
	// Write 从 p 向底层数据流写入 len(p) 个字节的数据
	// 返回实际写入的字节数 (0 <= n <= len(p))
	// 如果没有写完,那么会返回遇到的错误
	// 在 Write 返回 n < len(p) 时,err 必须是非 nil
	// Write 不允许修改 p 的数据,即使是临时修改
	//
	// 实现时不允许残留 p 的引用
	Write(p []byte) (n int, err error)
}
  • io.Writer 接口定义了 Fprintf 和调用者之间的约定:
    • 要求调用者提供的具体类型(如 *os.File 或者 *bytes.Buffer)包含一个与其签名和行为一致的 Write 方法
    • 这个约定保证了 Fprintf 能使用任何满足 io.Writer 接口的参数
  • 因为 fmt.Fprintf 仅依赖于 io.Writer 接口所约定的方法,对参数的具体类型没有要求,所以我们可以用任何满足 io.Writer 接口的具体类型作为 fmt.Fprintf 的第一个实参。
  • 这种可以把一种类型替换为满足同一接口的另一种类型的特性称为可取代性(substitutability)
  • 基于以上约定,可按如下创建一个新类型来测试这个特性:

bytecounter.go

type ByteCounter int

func (c *ByteCounter) Write(p []byte) (int, error) {
	*c += ByteCounter(len(p)) // 转换 Int 为 ByteCounter 类型
	return len(p), nil
}

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

	c = 0 // 重置计数器
	var name = "Dolly"
	// 因为 *ByteCounter 满足 io.Writer 接口的约定,所以可以在 Fprintf 中使用它,
	// Fprintf 察觉不到这种类型差异,ByteCounter 也能正确地累积格式化后结果的长度。
	fmt.Fprintf(&c, "hello, %s", name)
	fmt.Println(c) // "12", = len("hello, Dolly")
}
  • 除了 io.Writer 之外,fmt 包还有另一个重要的接口。Fprintf 和 Fprintln 提供了一个让类型控制如何输出自己的机制。定义一个 String 方法就可以让类型满足这个广泛使用的接口 fmt.Stringer:
package fmt
// 在字符串格式化时如果需要一个字符串
// 那么就调用这个方法来把当前值转化为字符串
// Print 这种不带格式化参数的输出方式也是调用这个方法
type Stringer interface {
	String() string
}

7.2、接口类型

  • 一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义中的所有方法。
  • 一个实现了接口中所有方法的具体类型是这个接口的实例(如上一小节的 ByteCounter)
  • io.Writer 是一个广泛使用的接口,负责所有可以写入字节的类型的抽象。
  • io 包还定义了很多有用的接口:
    • Reader 抽象了所有可以读取字节的类型。
    • Closer 抽象了所有可以关闭的类型。
package io
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}
  • 还有一些通过组合已有接口得到的新接口,例如:
type ReadWriter interface {
	Reader
	Writer
}

type ReadWriterCloser interface {
	Reader
	Writer
	Closer
}
  • 如上的语法称为嵌入式接口,类似于嵌入式结构,即可以直接使用一个接口,而不用逐一写出这个接口所包含的方法。或者如下:
// 方式二:
type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Writer(p []byte) (n int, err error)
}

// 方式三:
type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Writer
}
  • 真正有意义的只有接口的方法集合,方法声明的方式和定义的顺序都是无意义的。

7.3、实现接口

  • 如果一个类型实现了一个接口要求的所有方法,那么这个类型就实现了这个接口。
  • 通常说一个具体类型 “是一个” 特定的接口类型,这代表着该具体类型实现了该接口。比如:*bytes.Buffer 是一个 io.Writer;*os.File 是一个 io.ReaderWriter
  • 接口的赋值规则:仅当一个表达式实现了一个接口时,这个表达式才可以赋给该接口。
var w io.Writer
	w = os.Stdout         // OK: *os.File 有 Write 方法
	w = new(bytes.Buffer) // OK: *bytes.Buffer 有 Write 方法
	// Cannot use 'time.Second' (type Duration) as the type io.Writer Type does not implement 'io.Writer'
	// as some methods are missing: Write(p []byte) (n int, err error)
	// w = time.Second			// 编译错误: time.Duration 缺少 write 方法

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

	// 当右侧表达式也是一个接口时,该规则也有效
	w = rwc					// OK: io.ReadWriteCloser 有 Write 方法
	rwc = w					// 编译错误: io.Writer 缺少 Close 方法
  • 6.2 节提到,对每一个具体类型 T,部分方法的接收者是 T,而其它方法的接收者是 *T 指针。此时我们对类型 T 的变量直接调用 *T 的方法也可以是合法的,只要改变量是可变的,编译器隐式地帮忙完成了取地址的操作。
  • 例如,6.5 节提到的 IntSet 类型的 String 方法,需要一个指针接收者,所以无法从一个无地址的 IntSet 值上调用该方法:
type IntSet struct { /*...*/ }
func (*IntSet) String() string

var _ = IntSet{}.String() // 编译错误:String 方法需要 *IntSet 接收者
  • 但可以从一个 IntSet 变量上调用该方法:
var s IntSet
var _ = s.String() // OK: s 是一个变量,&s 有 String 方法
  • 因为只有 *IntSet 有 String 方法,所以也只有 *IntSet 实现了 fmt.Stringer 接口:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s  // 编译错误:IntSet 缺少 String 方法
  • 正如信封封装了信件,接口也封装了所对应的类型和数据,只有通过接口暴露的方法才可以调用,类型的其它方法则无法通过接口来调用:
os.Stdout.Write([]byte("hello"))	// OK: *os.File 有 Write 方法
os.Stdout.Close()					// OK: *os.File 有 Close 方法

var w io.Writer
w = os.Stdout
w.Write([]byte("hello"))			// OK: io.Writer 有 Write 方法
w.Close()							// 编译错误:io.Writer 缺少 Close 方法
  • 一个拥有更多方法的接口,比如 io.ReadWriter,相比于 io.Reader 能提供它所指向的数据的更多的信息,但是也会给实现这个接口提出更高的门槛。
  • 空接口类型 interface{},可以把任何值赋给空接口类型。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one":1}
any = new(bytes.Buffer)
  • 不能直接使用空接口类型中的值,因为这个接口不包含任何方法,我们需要一个方法从空接口中还原出实际值,在 7.10 中会通过类型断言来实现该功能。
  • 判定是否实现接口只需要比较具体类型和接口类型的方法。可以在注释中进行标注,如下:
// *bytes.Buffer 必须实现 io.Writer
var w io.Writer = new(bytes.Buffer)
  • 我们甚至不需要创建一个新的变量,因为 *bytes.Buffer 的任意值都实现了这个接口,包括 nil,在用 (*bytes.Buffer)(nil) 来强制类型转换后,也实现了这个接口。如果不想引用 w,那么可以把它替换为空白标识符:
// *bytes.Buffer 必须实现 io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)

7.4、使用 flag.Value 来解析参数

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

sleep.go

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()
}
  • 运行结果
    在这里插入图片描述

  • 默认的睡眠时间是1s,但可以用 -period 命令行标志来控制。

7.5、接口值

  • 接口值由两个部分组成:
    • 1.接口的动态类型:具体类型
    • 2.动态值:具体类型的一个值
  • 类型描述符:提供每个类型的具体信息
  • 在 Go 语言中,变量总是初始化为一个特定的值,接口也不例外。如下:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
  • 接口的零值就是把它的动态类型和值都设置为nil,如下图:
    在这里插入图片描述
  • 一个接口值是否为 nil 取决于它的动态类型。
  • 可以用 w == nil 或者 w != nil 来检测一个接口值是否为 nil。
  • 调用一个 nil 接口的任何方法都会导致崩溃 panic:
w.Write([]byte("hello"))	// 崩溃:对空指针取引用值
  • 语句 w = os.Stdout 把一个 *os.File 类型的值赋给了 w。
    • 这个过程把一个具体类型隐式转换为一个接口类型,等价于显示转换 io.Writer(os.Stdout)。
    • 接口值的动态类型会设置为指针类型 *os.File 的类型描述符,它的动态值会设置为 os.Stdout 的副本,即一个指向代表进程的标准输出的 os.File 类型的指针,如下图:
      在这里插入图片描述
  • 调用该接口值的 Write 方法,会实际调用 (*os.File).Write 方法,即输出 “hello”。
w.Write([]byte("hello"))	// "hello"
  • 语句 w = new(bytes.Buffer) 把一个 *bytes.Buffer 类型的值赋给了接口值:
    • 此时,动态类型是 *bytes.Buffer
    • 动态值是一个指向新分配缓冲区的指针,如下图:
      在这里插入图片描述
  • 调用 Write 方法的机制跟 w = os.Stdout 一样:
    • 此时,类型描述符是 *bytes.Buffer,所以调用的是 (*bytes.Buffer).Write 方法
    • 方法的接收者是缓冲区的地址
    • 调用该方法会追加 “hello” 到缓冲区
w.Write([]byte("hello"))	// 把 "hello" 写入 bytes.Buffer
  • 一个接口值可以指向多个任意大的动态值(非指针类型)。比如,time.Time 类型是一个包含几个非导出字段的结果。从它创建一个接口值的详情如下:
var x interface{} = time.Now()

在这里插入图片描述

  • 接口值可以用 == 和 != 操作符来做比较:
    • 如果两个接口值都是 nil 或者二者的动态类型完全一致且二者动态值相等(使用动态类型的 == 操作符来做比较),那么两个接口值相等。
    • 根据接口值的可比较性,所以它们可以作为 map 的键,也可以作为 switch 语句的操作数。
    • 如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如 slice),那么将它们进行的比较会发生 panic:
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // 宕机:试图比较不可比较的类型 []int
  • 这么看,接口类型是非平凡的:
    • 其它类型要么是可以安全比较的(比如基础类型和指针),要么是完全不可比较的(比如 slice、map 和函数)
    • 但当比较接口值或者其中包含接口值的聚合类型时,我们必须小心崩溃的可能性。
    • 当把接口作为 map 的键或者 switch 语句的操作数时,也存在类似的风险。
    • 仅在能确认接口值包含的动态值可以比较时,才比较接口值。
  • 当处理错误或者调试时,能拿到接口值动态类型是很有帮助的。可以使用 fmt 包的 %T 来实现这个需求:
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"
  • 在内部实现中,fmt 用反射来拿到接口动态类型的名字。

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

  • 考虑如下程序,当 debug 设置为 true 时,主函数收集函数 f 的输出到一个缓冲区中:
const debug = true

func main() {
	var buf *bytes.Buffer
	if debug {
		buf = new(bytes.Buffer)	// 启用输出收集
	}
	f(buf)	// 注意:微妙的错误
	if debug {
		// ...使用 buf...
	}
}

// 如果 out 不是 nil,那么会向其写入输出的数据
func f(out io.Writer) {
	// ...其它代码
	if out != nil {
		out.Write([]byte("done!\n"))
	}
}
  • 当设置 debug 为 false 时,我们会觉得仅仅是不再收集输出,但实际上会导致程序在调用 out.Write 时崩溃:
if out != nil {
	out.Write([]byte("done!\n"))	// 可能会宕机:对空指针取引用值
}
  • 当 main 函数调用 f 时,它把一个类型为 *bytes.Buffer 的空指针赋给了 out 参数,所以 out 的动态值确实为空。但它的动态类型是 *bytes.Buffer,这表示 out 是一个包含空指针的非空接口(如图7.5),所以防御性检查 out != nil 仍然是 true。
    在这里插入图片描述
  • 如前所述,动态分发机制决定了我们肯定会调用 (*bytes.Buffer) .Write,只不过这次接收者值为空。对于某些类型,比如 *os.File,空接收值是合法的(6.2.1节),但对于 *bytes.Buffer 则不行。方法尽管被调用了,但在尝试访问缓冲区时崩溃了。
  • 这个调用违背了 (*bytes.Buffer).Write 的一个隐式的前置条件,即接收者不能为空,所以把空指针赋给这个接口就是一个错误。
  • 解决方案:把 main 函数中的 buf 类型修改为 io.Writer,从而避免在最开始就把一个功能不完整的值赋给一个接口。
var buf io.Writer
if debug {
	buf = new(bytes.Buffer)	// 启用输出收集
}
f(buf)	// OK
  • 接下来要介绍 Go 标准库的一些重要接口。

7.6、使用 sort.Interface 来排序

  • 一个原地排序算法需要知道三个信息:序列长度、比较两个元素的含义以及如何交换两个元素,所以 sort.Interface 接口就有三个方法:
package sort

type Interface interface {
	Len() int
	Less(i, j int) bool	// i, j 是序列元素的下标
	Swap(i, j int)
}
  • 要对序列排序,需要先确定一个实现了如上三个方法的类型,接着把 sort.Sort 函数应用到上面这类方法的实例上。
  • 先考虑一个最简单的例子:字符串 slice。定义的新类型 StringSlice 以及它的三个方法如下:
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] }
  • 现在就可以对一个字符串 slice 进行排序,只需简单地把一个 slice 转换为 StringSlice 类型即可:sort.Sort(StringSlice(names))
  • 类型转换生成了一个新的 slice,与原始的 names 有同样的长度、容量和底层数组,不同的就是额外增加了三个由于排序的方法。
  • 字符串 slice 的排序太常用了,所以 sort 包提供了 StringSlice 类型,以及一个直接排序的 Strings 函数,于是上面的代码可以简写为 sort.Strings(names)
  • 如下变量 tracks 包含一个播放列表。每个元素都是一个指向 Track 的指针。考虑到 sort 函数要交换很多对元素,所以在元素是一个指针的情况下代码运行速度会更快,毕竟一个指针的大小只有一个字长,而一个完整的 Track 则需要8个甚至更多的字。
package main

import (
	"fmt"
	"os"
	"sort"
	"text/tabwriter"
	"time"
)

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

var tracks = []*Track{
	{"Go", "XiaoMing", "Music of Xiao Ming", 2012, length("3m38s")},
	{"Go", "XiaoHong", "Music of Xiao Hong", 1992, length("3m37s")},
	{"Go Ahead", "XiaoJun", "Music of Xiao Jun", 2007, length("4m36s")},
	{"Ready 2 Go", "XiaoWang", "Music of Xiao Wang", 2011, length("4m24s")},
}

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

// 将播放列表输出为一个表格
func printTracks(tracks []*Track) {
	const format = "%v\t%v\t%v\t%v\t%v\t\n"
	// 使用 text/tabwriter 包可以生成一个表格
	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 类型,以及必须的 Len、Less 和 Swap 方法
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] }

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() {
	fmt.Println("byArtist:")
	sort.Sort(byArtist(tracks))
	printTracks(tracks)

	fmt.Println("\nReverse(byArtist):")
	sort.Sort(sort.Reverse(byArtist(tracks)))
	printTracks(tracks)

	fmt.Println("\nbyYear:")
	sort.Sort(byYear(tracks))
	printTracks(tracks)

	fmt.Println("\nCustom:")
	// 定义一个多层的比较函数,先按照标题title排序,接着是年份year,最后是时长length
	// 如下 sort 调用就是一个使用匿名排序函数来这样排序的例子:
	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)
}

// 实现 sort.Interface 的具体类型并不一定都是 slice,比如 customSort 就是一个结构类型:
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] }

func init() {
	// 为了简便起见,sort 包专门提供了对于 []int、[]string、[]float64 自然排序的函数和相关类型
	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"
}
  • 运行结果
    在这里插入图片描述

7.7、http.Handler 接口

  • 本节会进一步讨论服务端 API,以及作为其基础的 http.Handler 接口

net/http

package http

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

func ListenAndServe(address string, h Handler) error
  • ListenAndServe 函数需要一个服务器地址,比如 “localhost:8080”,以及一个 Handler 接口的实例(用来接受所有的请求)。这个函数会一直运行,直到服务出错(或者启动时就失败了)时返回一个非空的错误。
  • 现有一个电子商务网站,使用一个数据库来存储商品和价格(以美元计价)的映射,实现的程序如下。它用一个 map 类型(命名为 database)来代表仓库,再加上一个 ServeHTTP 方法来满足 http.Handler 接口。这个函数遍历整个 map 并且输出其中的元素:

http1.go

func main() {
	db := database{"shoes": 50, "socks": 5}
	log.Fatal(http.ListenAndServe("localhost:8080", 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)
	}
}

在这里插入图片描述

  • 一个更真实的服务器会定义多个不同 URL,每个出发不同的行为。我们把现有功能的 URL 设为 /list,再加上另一个 /price 用来显示单个商品的价格,商品可以在请求参数中指定,比如 /price?item=socks

http2.go

package main

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

func main() {
	db := database{"shoes": 50, "socks": 5}
	log.Fatal(http.ListenAndServe("localhost:8080", 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) {
	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) // 404
			fmt.Fprintf(w, "no such item: %q\n", item)
			return
		}
		fmt.Fprintf(w, "%s\n", price)
	default:
		w.WriteHeader(http.StatusNotFound) // 404
		fmt.Fprintf(w, "no such page: %s\n", req.URL)
	}
}

7.10、类型断言

  • 类型断言:
    • 定义:是一个作用在接口值上的操作。
    • 格式:x.(T)。其中 x 是一个接口类型的表达式,而 T 是一个类型(称为断言类型)。
    • 作用:检查作为操作数的动态类型是否满足指定的断言类型
      • 情况一:若断言类型 T 是一个具体类型,那么类型断言会检查 x 的动态类型是否就是 T。如果检查成功,类型断言的结果就是 x 的动态值,类型就是 T。换句话说,类型断言就是用来从它的操作数中把具体类型的值提取出来的操作。如果检查失败,那么操作崩溃。
      • 情况二:若断言类型 T 是一个接口类型,那么类型断言会检查 x 的动态类型是否满足 T。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,接口值的类型和值部分也不变,只是结果的类型为接口类型 T。换句话说,类型断言是一个接口值表达式,从一个接口类型变为拥有另一套方法的接口类型(通常方法数量增多),但保留了接口值中的动态类型和动态值部分。
// 情况一:
var w io.Writer
w = os.Stdout
f := w.(*os.File)		// 成功:f == os.Stdout
c := w.(*bytes.Buffer)	// 崩溃:接口持有的是 *os.File,不是 *bytes.Buffer
// 情况二:
// 此时 w 和 rw 都持有 os.Stdout,于是所有对应的动态类型都是 *os.File,但 w 作为 io.Writer
// 仅暴露了文件的 Write 方法,而 rw 还暴露了它的 Read 方法。
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter)	// 成功:*os.File 有 Read 和 Write 方法
w = new(ByteCounter)
rw = w.(io.ReadWriter)	// 崩溃:*ByteCounter 没有 Read 方法
  • 无论哪种类型作为断言类型,如果操作数是一个空接口值nil,类型断言都失败。
w = rw				// io.ReadWriter 可以赋给 io.Writer
w = rw.(io.Writer)	// 仅当 rw == nil 时失败
  • 若无法确定一个接口值的动态类型,这时就需要进行检测。如果类型断言出现在需要两个结果的赋值表达式(如下)中,那么断言不会在失败时崩溃,而是会多返回一个布尔型的返回值来指示断言是否成功。
var w io.Writer = os.Stdout
f, ok := w.(*os.File)		// 成功:ok, f == os.Stdout
b, ok := w.(*bytes.Buffer)	// 失败:!ok, b == nil
  • ok 返回值常用于决定下一步做什么
if f, ok := w.(*os.File); ok {
	// ...使用 f...
}
  • 当类型断言的操作数是一个变量时,若返回值的名字与操作数变量名一致,原有的值就会被新的返回值覆盖,如下:
if w, ok := w.(*os.File); ok {
	// ...use w...
}

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

  • I/O 操作会因为很多原因失败,但有三类原因需单独处理:文件已存储(创建操作)、文件没找到(读取操作)以及权限不足。
  • os 包提供了三个帮助函数用来对错误进行分类:
package os

func IsExist(err error) bool
func IsNotExise(err error) bool
func IsPermission(err error) bool
  • 检查方法:
package 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 的结构保留了错误所有的底层信息
func main() {
	_, err := os.Open("/no/such/file")
	fmt.Println(err)		// "open /no/such/file: No such file or directory"
	fmt.Printf("%#v\n", err)
	// 输出:
	// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
}
  • 如下所示的 IsNotExist 判断错误是否等于 syscall.ENOENT(参见7.8节),或等于错误 os.ErrNotExist(参见5.4.2节的 io.EOF),或是一个 *PathError,并且底层的错误是上面二者之一。
import (
	"errors"
	"fmt"
	"os"
	"syscall"
)

var ErrNotExist = errors.New("file dose not exist")

// IsNotExist 返回一个布尔值,该值表明错误是否代表文件或目录不存在
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist 和其他一些会返回 true 的系统调用错误
func IsNotExist(err error) bool {
	if pe, ok := err.(*os.PathError); ok {
		err = pe.Err
	}
	return err == syscall.ENOENT || err == ErrNotExist
}

// 实际使用情况如下:
func main() {
	_, err := os.Open("/no/such/file")
	fmt.Println(os.IsNotExist(err)) // "true"
}

7.13、类型分支

  • 接口有两种不同的风格:
    • 第一种风格:典型的如 io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler 和 error,接口上的各种方法突出了满足这个接口的具体类型之间的相似性,但隐藏了各个具体类型的布局和各自特有的功能。这种风格强调了方法,而不是具体类型。
    • 第二种风格:利用了接口值能够容纳各种具体类型的能力,把接口作为这些类型的联合(union)来使用。类型断言用来在运行时区分这些类型并分别处理。这种风格强调的是满足这个接口的具体类型,而不是这个接口的方法或者信息隐藏。这种风格的接口使用方式称为可识别联合(discriminated union)
    • 两种风格分别对应面向对象编程中的子类型多态和特设多态。
    • 一个 switch 语句可以把包含一长串值相等比较的 if-else 语句简化掉。一个相似的类型分支语句则可以用来简化一长串的类型断言 if-else 语句。
switch x.(type) {
case nil:		// ...
case int, uint:	// ...
case bool:		// ...
case string:	// ...
default:		// ...
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言圣经》是一本广受好评的教材,旨在帮助读者系统地学习和掌握Go语言。这本书以简明清晰的方式介绍了Go语言的各种特性、语法和用法,是入门和进阶者的理想选择。 首先,该书提供了对Go语言基础知识的全面介绍。它从简单到复杂地解释了Go语言的基本概念,诸如变量、函数、循环和条件语句等等。通过丰富的例子和练习,读者能够逐步理解和掌握这些概念。 其次,该书详细介绍了Go语言的高级特性和用法。读者可以学习到Go语言的面向对象编程、并发编程、网络编程等关键技术。并发编程是Go语言的一个独特特性,对于提高程序性能和扩展能力非常重要。 此外,该书还包含了对常见问题和陷阱的讲解,帮助读者避免一些常见的错误和陷阱。同时,书中提供了大量的案例和实践项目,读者可以通过实际操作来巩固所学内容。 《Go语言圣经》以其简洁明了的风格和对细节的深入讲解而闻名。无论是作为初学者的入门指南,还是作为有经验的开发者的参考书,这本书都能满足读者的需求。此外,该书的PDF版本能够方便地在线或离线阅读,为读者提供了更加便捷的学习体验。 综上所述,《Go语言圣经》是一本内容丰富、权威性强的优秀教材。它不仅适合Go语言的初学者,也适用于那些想要深入学习和掌握Go语言的开发者。无论是在线阅读还是PDF版本,读者都能够方便地获取和利用这本宝贵的学习资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值