【Go语言圣经】第七节:接口

server/2025/2/5 10:47:40/

第七章:接口

Golang 当中接口类型的独特之处在于它是满足隐式实现的。即:没必要对于给定的具体类型定义所有满足的接口类型,简单地拥有一些必要的方法即可。这种设计使得我们可以创建一个新的接口类型来满足已经存在的具体类型,却不会改变这些类型的定义。

7.1 接口约定

Golang 当中存在一种名为“接口”(interface)的抽象类型。它不会暴露它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,它只会表现出它们自己的方法。也就是说,当我们看到一个接口类型时,我们不知道它具体是什么,但是我们知道可以通过它的方法来做什么

例如,我们经常使用的fmt.Printffmt.Sprintf都是通过fmt.Fprintf来进行封装的,fmt.Fprintf这个函数对于它的计算结果将会被怎么使用是不知道的:

package fmtfunc Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {var buf bytes.BufferFprintf(&buf, format, args...)return buf.String()
}

对于Fprintf的第一个参数,它不是一个文件类型,而是io.Writer类型,它是一个接口,它的定义如下:

package io// Writer is the interface that wraps the basic Write method.
type Writer interface {// Write writes len(p) bytes from p to the underlying data stream.// It returns the number of bytes written from p (0 <= n <= len(p))// and any error encountered that caused the write to stop early.// Write must return a non-nil error if it returns n < len(p).// Write must not modify the slice data, even temporarily.//// Implementations must not retain p.Write(p []byte) (n int, err error)
}

io.Writer类型定义了函数Fprintf和这个函数调用者(比如PrintfSprintf)之间的约定。一方面这个约定需要调用者提供具体类型的值,比如*os.File*bytes.Buffer,这些类型都有一个Write方法。另一方面这个约定确保Fprintf接受任何满足io.Writer接口的值都可以工作。

Fprintf函数可能没有假定写入的是一个文件或一段内存,而是写入了一个可以调用Write函数的值。

由于fmt.Fprintf没有对具体操作的值做任何假设,而仅仅通过io.Writer接口的约定来保证行为,所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值(意思是传入的值只要具备Write方法,这个函数调用就是安全的)。一个类型可以自由地被另一个满足相同接口的类型替换,称为可替换性,这是一个面向对象的特征。

下例设计了一个新的类型来检验上述理论,其声明的*ByteCounter类型里有一个Write方法:

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

由于*ByteCounter满足io.Writer的约定,可以把它传入Fprintf函数当中:

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

7.2 接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

io.Writer是最广泛使用的接口之一,因为它提供了所有类型的写入 bytes 的抽象。io 包中定义了许多其它有用的接口类型。Reader 可以代表任意可以读取 bytes 的类型,Closer 可以是任意可以关闭的值,例如一个文件或网络连接:

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

可以通过接口的组合来定义新的接口类型:

type ReadWriter interface {Reader Writer
}
type ReadWriteCloser interface {ReaderWriterCloser
}

上面用到的语法与结构定义当中的内嵌类似,我们当然可以将内嵌的方法展开,但略失简洁。可以混合使用展开定义和内嵌定义。

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

7.3 实现接口的条件

一个类型如果拥有一个接口所需的所有方法,那么这个类型就实现了这个接口。例如*os.File实现了io.ReaderWriterCloserReadWriter等接口。Golang 程序员通常会简要地把一个具体的类型描述成一个特定的接口类型。

接口的指定规则很简单,正如前面反复提到的,表达一个类型属于某个接口只需要这个类型实现了这个接口:

var w io.Writer
w = os.Stdout			// OK
w = new(bytes.Buffer)	// OK
w = time.Second			// compile error: time.Duration lacks Write methodvar rwc io.ReadWriteCloser
rwc = os.Stdout			// OK
rwc = new(bytes.Buffer)	// compile error: *bytes.Buffer lacks Close method// 上述规则适用于等式右侧本身也是一个接口的情况
w = rwc					// OK
rwc = w					// compile error: io.Writer lacks Close method

接口类型封装和隐藏了具体类型和它的值,即使具体类型有其它的方法,也只有接口暴露出来的方法可以被调用:

os.Stdout.Write([]byte("hello"))	// OK
os.Stdout.Close()					// OKvar w io.Writer
w = os.Stdout
w.Write([]byte("hello"))			// Ok
w.Close()							// compile error: io.Writer lacks Close method

对于没有定义方法的接口interface{},它被称为空接口类型,它是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型(甚至,一个接受空接口类型的切片可以存储任意值):

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

对于创建的一个interface{},我们不能直接对它持有的值进行操作,因为interface{}没有任何方法。

由于接口和实现只依赖于判断两个类型的方法是否相同,所以没必要定义一个具体类型和它实现的接口之间的关系。即:有意地在文档里说明或者程序上断言这种关系偶尔是有用的,但程序不强制这么做。下面的定义在编译器断言一个*bytes.Buffer的值实现了io.Writer接口类型:

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

因为任意*bytes.Buffer的值,甚至 nil 通过 (*bytes.Buffer)(nil)进行显式的转换都能实现这个接口,所以我们不必为其分配一个新的变量。

一个具体的类型可能实现了很多不相关的接口。例如,下例是一个数字文化产品销售的例子,以下列出了产品具有的具体类型:

Album
Book
Movie
Magazine
Podcast
TVEpisode
Track

可以把每个抽象的特点用接口来表示:

type Artifact interface {Title() stringCreators() []stringCreated() time.Time
}

一些特性只有特定的产品才具有:

type Text interface {Pages() intWords() intPageSize() int
}
type Audio interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string // e.g., "MP3", "WAV"
}
type Video interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string // e.g., "MP4", "WMV"Resolution() (x, y int)
}

可以定义一个 Streamer 接口来代表它们之间相同的部分而不必对已经存在的类型进行改变:

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

7.4 flag.Value 接口

使用flag.Duration可以完成时间周期的标记,这个特性被构建到了 flag 包中。如果我们想要为自己的数据定义新的标记符号,只需要定义一个实现flag.Value接口的类型:

package flagtype Value interface {String() stringSet(string) error
}

String 方法格式化标记的值用在命令行帮助消息中;这样每一个 flag.Value 也是一个 fmt.StringerSet方法解析它的字符串参数并且更新标记变量的值。

下例定义了一个允许通过摄氏度或华氏度变换的形式指定温度的 celsiusFlag 类型。由于 celsiusFlag 内嵌了 Celsius 类型,因此它本身已经具有 String 方法了

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

下面的 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 = tempconv.CelsiusFlag("temp", 20.0, "the temperature")func main() {flag.Parse()fmt.Println(*temp)
}

7.5 接口值

接口值由两部分组成,分别是一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于 Golang 这种静态语言,类型是编译器的概念,因此一个类型不是一个值。

下面的四个语句中,变量 w 得到了 3 个值:

var w io.Writer // 此时默认为零值 nil
w = os.Stdout
w = new(bytes.Buffer)
w = nil

第二个语句将*os.File类型的值赋给了变量w:

w = os.Stdout

这个赋值过程调用了一个具体类型到接口类型的隐式转换,它和显式地使用io.Writer(os.Stdout)是等价的。这种转换不管是显式还是隐式的,都会刻画出操作到的类型和值。接口值的动态类型(type)被设为*os.File指针的类型描述符,它的动态值(value)持有os.Stdout的拷贝:
在这里插入图片描述

7.5.1 警告:一个包含 nil 指针的接口不是 nil 接口

一个不包含任何值的 nil 接口值和一个刚好包含 nil 指针的接口值是不同的。

7.6 sort.Interface 接口

Golang 的 sort.Sort 函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定。

一个内置的排序算法需要知道三样东西:序列的长度、表示两个元素比较的结果、一种交换两个元素的方式,这就是sort.Interface的三个方法:

package sorttype Interface interface {Len() intLess(i, j int) boolswap(i, j int)
}

为了对序列进行排序,我们需要定义一个实现了上述三个方法的类型,然后对这个类型的一个实例应用sort.Sort函数:

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

可以通过下面这样的方式对一个 string 切片排序:

sort.Sort(StringSlice(names))

7.7 http.Handler 接口

本小节会对那些基于 http.Handler 接口的服务器 API 进行进一步的学习:

package httptype Handler interface {ServeHTTP(w ResponseWriter, r *Request)
}func ListenAndServe(address string, h Handler) error

ListenAndServe函数需要一个例如"localhost:8080"这样的服务器地址,和一个所有请求都可以分派的Handler接口实例。它会一直运行直到这个服务因为一个错误而失败,它的返回值是一个非空错误。

下例实现了一个简单的电子商务网站,它将库存清单模型化为一个命名为 database 的 map 类型,我们给这个类型一个 ServeHttp 方法,这样它就可以满足 http.Handler 接口,这个 handler 会遍历整个 map 并输出物品信息:

func main() {db := database{"shoes": 50, "socks": 5}log.Fatal(http.ListenAndServe("localhost:8000", db))
}type dollars float32func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }type database map[string]dollarsfunc (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {for item, price := range db {fmt.Fprintf(w, "%s: %s\n", item, price)}
}

目前为止这个服务器不考虑 URL,只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的 URL,每一个都会触发一个不同的行为:

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) // 404fmt.Fprintf(w, "no such item: %q\n", item)return}fmt.Fprintf(w, "%s\n", price)default:w.WriteHeader(http.StatusNotFound) // 404fmt.Fprintf(w, "no such page: %s\n", req.URL)}
}

现在的 handler 会根据 URL 路径部分来决定执行什么逻辑。

7.8 error 接口

在学习这本书的过程中已经大量接触过 error 类型。它实际上是一个 interface 类型,这个类型有一个返回错误信息的单一方法:

type error interface {Error() string
}

创建一个 error 最简单的方法就是调用 errors.New 函数,它会根据传入的错误信息返回一个新的 error。整个 errors 包只有四行:

package errorsfunc New(text string) error { return &errorString{text} }type errorString struct { text string }func (e *errorString) Error() string { return e.text }

调用 errors.New 的机会很少,因为有一个封装好的 fmt.Errorf

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

7.9 示例:表达式求值

本节会构建一个简单的算术表达式求值器。我们将使用一个接口 Expr 来表示 Golang 中的任意表达式:

type Expr interface{}	// 现在这个接口不需要方法, 稍后我们会为它定义一些

表达式的例子如下:

sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9

下面是五个具体的表达式类型。Var 类型表示对一个变量的引用。literal 类型表示一个浮点型常量。unary 和 binary 表示一到两个运算对象的运算符表达式,这些操作数可以是任意的 Expr 类型。call 类型表示对一个函数的调用:

type Var stringtype literal float64type unary struct {op rune	// + / -x Expr
}type binary struct {op rune	// + / - / × / ÷x, y Expr
}type call struct {fn string	// pow / sin / sqrtargs []Expr
}

为了计算包含变量的表达式,需要一个 environment 变量将变量的名字映射为对应的值:

type Env map[Var]float64

需要每个表达式定义一个 Eval 方法,这个方法会根据 environment 变量返回表达式的值。因为每个表达式都提供这个方法,因此将它加入到 Expr 接口中。这个包只会对外公开 Expr、Env、Var 类型:

type Expr interface {Eval(env Env) float64
}

具体的 Eval 方法如下:

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.x.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 "pow":return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))case "sin":return math.Sin(c.args[0].Eval(env))case "sqrt":return math.Sqrt(c.args[0].Eval(env))}panic(fmt.Sprintf("unsupported function call: %s", c.fn))
}

现在我们向 Expr 接口添加一个 Check 方法,它会对一个表达式语法树检查静态错误:

type Expr interface {Eval(env Env) float64// Check reports errors in this Expr and adds its Vars to the set.Check(vars map[Var]bool) error
}

具体的 Check 方法;

func (v Var) Check(vars map[Var]bool) error {vars[v] = truereturn nil
}func (literal) Check(vars map[Var]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}

7.10 类型断言

类型断言是一个使用在接口值上的操作。语法上看起来像x.(T),此处的x表示一个接口类型而T表示一个类型。类型断言会检查它操作对象的动态类型是否和断言的类型匹配。

类型断言有两种可能。第一种,如果断言的类型T是一个具体类型,类型断言检查x的动态类型是否和T相同。如果检查成功,那么断言的结果是x的动态值,当然它的类型是T。检查失败则抛出 panic:

var w io.Writer
w = os.Stdout
f := w.(*os.File)
c := w.(*bytes.Buffer)

一个替代的方案如下,使用 ok 作为类型断言的第二个接收值,此时当类型断言失败时,不会产生 panic:

var w io.Writer = os.Stdout
f, ok := w.(*os.File)
b, ok := w.(*bytes.Buffer)

7.11 基于类型断言区别错误类型

os 包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:

package osfunc IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool

一个更可靠的方式是使用一个专门的类型来描述结构化的错误:

package os// PathError records an error and the operation and file path that caused it.
type PathError struct {Op   stringPath stringErr  error
}func (e *PathError) Error() string {return e.Op + " " + e.Path + ": " + e.Err.Error()
}

7.12 通过类型断言询问行为

7.13 类型分支

7.14 示例:基于标记的 XML 解码

7.15 一些建议

(7.12 ~ 7.14 这部分中文版资源机翻的痕迹太明显,读起来有些晦涩,打算真正需要用到的时候再说)
接口只有当有两个或以上的具体类型必须以相同的方式进行处理时才需要。


http://www.ppmy.cn/server/165132.html

相关文章

EtherCAT主站IGH-- 28 -- IGH之ioctl.h/c文件解析

EtherCAT主站IGH-- 28 -- IGH之ioctl.h/c文件解析 0 预览一 该文件功能`cdev.c` 文件功能函数预览二 函数功能介绍`cdev.c` 中主要函数的作用1. `ec_cdev_init`2. `ec_cdev_clear`3. `eccdev_open`4. `eccdev_release`5. `eccdev_ioctl`6. `eccdev_mmap`7. `eccdev_vma_fault`8…

CSS布局(一)flex一篇搞定

目录 一、flex布局 1.1. 认识flex布局 1.2. flex布局重要的概念 二、flex container中的属性 2.1.flex-direction 2.2.flex-wrap、flex-flow 2.3.justify-content 2.4.align-items 2.5.align-content 三、 flex item中的属性 3.1.order 3.2.align-self 3.3.flex-gr…

[Collection与数据结构] B树与B+树

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

浅谈《图解HTTP》

感悟 滑至尾页的那一刻&#xff0c;内心突兀的涌来一阵畅快的感觉。如果说从前对互联网只是懵懵懂懂&#xff0c;但此刻却觉得她是如此清晰而可爱的呈现在哪里。 介绍中说&#xff0c;《图解HTTP》适合作为第一本网络协议书。确实&#xff0c;它就像一座桥梁&#xff0c;连接…

【PyQt】keyPressEvent键盘按压事件无响应

问题描述 通过load ui 文件加载程序时&#xff0c;keyPressEvent键盘按压事件无响应 原因 主要是由于事件处理的方式和窗口的显示方式不正确所导致的。 解决代码 self&#xff1a;这里的self作为loadUi函数的第二个参数&#xff0c;意味着加载的界面将被设置为当前类实例&…

Vue.js 如何选择合适的组件库

Vue.js 如何选择合适的组件库 大家在开发 Vue.js 项目的时候&#xff0c;都会面临一个问题&#xff1a;我该选择哪个组件库&#xff1f; 市面上有很多优秀的 Vue 组件库&#xff0c;比如 Element Plus、Vuetify、Quasar 等&#xff0c;它们各有特点。选择合适的组件库&#xf…

系统思考与心智模式

“人们往往高估短期变化&#xff0c;却低估长期系统的力量。”——杰伊福雷斯特 我们总是希望一场会议能改变企业文化&#xff0c;一场头脑风暴能重塑战略&#xff0c;一项新决策能立刻见效。但现实是&#xff0c;短期的调整只是表象&#xff0c;真正决定结果的&#xff0c;是…

【归属地】批量号码归属地查询按城市高速的分流,基于WPF的解决方案

在现代商业活动中&#xff0c;企业为了提高营销效果和资源利用效率&#xff0c;需要针对不同地区的市场特点开展精准营销。通过批量号码归属地查询并按城市分流&#xff0c;可以为企业的营销决策提供有力支持。 短信营销&#xff1a;一家连锁餐饮企业计划开展促销活动&#xf…