接口既约定
Go 语言中接口是抽象类型 ,与具体类型不同 ,不暴露数据布局、内部结构及基本操作 ,仅提供一些方法 ,拿到接口类型的值 ,只能知道它能做什么 ,即提供了哪些方法 。
go
func 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.Buffer
Fprintf(&buf, format, args...)
return buf.String()
}
fmt.Fprintf
函数用于格式化输出 ,其第一个形参是io.Writer
接口类型 。io.Writer
接口封装了基础写入方法 ,定义了Write
方法 ,要求将数据写入底层数据流 ,并返回实际写入字节数等 。
go
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)
}
- 这一接口定义了
fmt.Fprintf
和调用者之间的约定 ,调用者提供的具体类型(如*os.File
、*bytes.Buffer
)需包含与Write
方法签名和行为一致的方法 ,保证fmt.Fprintf
能使用满足该接口的参数 ,体现了可取代性 ,即只要满足接口 ,具体类型可相互替换 。
go
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p))
return len(p), nil
}
- 以
ByteCounter
类型为例 ,它实现了Write
方法 ,满足io.Writer
接口约定 ,可在fmt.Fprintf
中使用 。
go
package fmt
// 在字符串格式化时如果需要一个字符串
// 那么就调用这个方法来把当前值转化为字符串
// Print 这种不带格式化参数的输出方式也是调用这个方法
type Stringer interface {
String() string
}
fmt
包还有fmt.Stringer
接口 ,定义了String
方法 。类型实现该方法 ,就能在字符串格式化时将自身转化为字符串输出 ,如之前的Celsius
、*IntSet
类型添加String
方法后 ,可满足该接口 ,实现特定格式输出 。
接口类型
接口类型定义了一套方法 ,具体类型要实现某接口 ,必须实现该接口定义的所有方法 。如io.Writer
接口抽象了所有可写入字节的类型 ,像文件、内存缓冲区等 ,具体类型若要符合io.Writer
,就得实现其Write
方法 。
go
package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
Reader
接口 :抽象了所有可读取字节的类型 ,定义了Read
方法 ,用于从类型中读取数据 。Closer
接口 :抽象了所有可关闭的类型 ,如文件、网络连接等 ,定义了Close
方法 。
go
type ReadWriter interface {
Reader
Writer
}
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
}
- 可通过组合已有接口得到新接口 ,如
ReadWriter
接口由Reader
和Writer
接口组合而成 ,ReadWriteCloser
接口由Reader
、Writer
和Closer
接口组合而成 ,这种方式称为嵌入式接口 ,类似嵌入式结构 ,可直接使用组合后的接口 ,无需逐一写出其包含的方法 。 - 三种声明效果一致 ,方法定义顺序无意义 ,关键是接口的方法集合 。
实现接口
- 具体类型实现接口需实现接口定义的所有方法 ,如
*os.File
实现了io.Reader
、Writer
、Closer
和ReaderWriter
接口 ,*bytes.Buffer
实现了Reader
、Writer
和ReaderWriter
接口 。
go
var w io.Writer
w = os.Stdout // OK: *os.File有Write方法
w = new(bytes.Buffer) // OK: *bytes.Buffer有Write方法
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方法
- 接口赋值规则 :当表达式实现接口时可赋值给对应接口类型变量 ,若右侧表达式也是接口 ,要满足接口间方法包含关系 。如
io.ReadWriteCloser
接口包含io.Writer
接口方法 ,实现前者的类型也实现了后者 。
go
type IntSet struct { /*... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // 编译错误: String 方法需要*IntSet 接收者
var s IntSet
var _ = s.String() // OK: s 是一个变量,&s有 String 方法
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // 编译错误: IntSet缺少String 方法
- 类型方法与接口实现的关系 :类型的方法接收者有值类型和指针类型 ,编译器可隐式处理值类型变量调用指针方法(前提变量可变 ) 。如
IntSet
类型String
方法接收者为指针类型 ,不能从无地址的IntSet
值调用 ,但可从IntSet
变量调用 ,且*IntSet
实现了fmt.Stringer
接口 。
go
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
interface{}
为空接口类型 ,对实现类型无方法要求 ,可把任何值赋给它 ,如fmt.Println
、errorf
等函数能接受任意类型参数就是利用了空接口 。但不能直接使用空接口值 ,需通过类型断言等方法还原实际值 。
隐式声明
- 编译器可隐式判断类型实现接口 ,如
*bytes.Buffer
的任意值(包括nil
)都实现了io.Writer
接口 ,可简化变量声明 。非空接口常由指针类型实现 ,但也有其他引用类型可实现接口 ,如slice
、map
、函数类型等 ,且基本类型也能通过定义方法实现接口 ,如time.Duration
实现了fmt.Stringer
。
go
type Text interface {
Pages() int
Words() int
PageSize() int
}
type Audio interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // 比如 "MP3"、"WAV"
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // 比如 "MP4"、"WMV"
Resolution() (x, y int)
}
type Streamer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
- 接口用于抽象分组 :以管理或销售数字文化商品的程序为例 ,定义多种具体类型(如
Album
、Book
等 ) ,可针对不同属性定义接口(如Artifacts
、Pages
、Audio
、Video
) ,还可进一步组合接口(如Streamer
) 。Go 语言可按需定义抽象和分组 ,不用修改原有类型定义 ,从具体类型提取共性用接口表示 。
使用 flag.Value 来解析参数
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()
}
以实现睡眠指定时间功能的程序为例 ,通过flag.Duration
函数创建time.Duration
类型的标志变量period
,用户可用友好方式指定时长 ,如50ms
、2m30s
等 ,程序进入睡眠前输出睡眠时长 。
go
package flag
// Value 接口代表了存储在标志内的值
type Value interface {
String() string
Set(string) error
}
flag.Value
接口定义了String
和Set
方法 。String
方法用于格式化标志对应的值 ,输出命令行帮助消息 ,实现该接口的类型也是fmt.Stringer
;Set
方法用于解析传入的字符串参数并更新标志值 ,是String
方法的逆操作 。
自定义实现flag.Value
接口
go
// *celsiusFlag 满足 flag.Value 接口
type celsiusFlag struct{ Celsius }
func (f *celsiusFlag) Set(s string) error {
var unit string
var value float64
fmt.Sscanf(s, "%f%s", &value, &unit) // 无须检查错误
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
类型 ,内嵌Celsius
类型 ,因已有String
方法 ,只需实现Set
方法来满足flag.Value
接口 。Set
方法中 ,使用fmt.Sscanf
从输入字符串解析浮点值和单位 ,根据单位(C
或F
)进行摄氏温度和华氏温度转换 ,若输入无效则返回错误 。
go
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
f := celsiusFlag{value}
flag.CommandLine.Var(&f, name, usage)
return &f.Celsius
}
CelsiusFlag
函数封装相关逻辑 ,返回指向内嵌Celsius
字段的指针 ,并通过flag.CommandLine.Var
方法将标志加入命令行标记集合 。
接口值
接口值由具体类型(动态类型 )和该类型对应的值(动态值 )两部分组成 。在 Go 语言中 ,类型是编译时概念 ,不是值 ,接口值的类型部分用类型描述符表示 。
go
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
- 初始化 :接口变量初始化为零值 ,即动态类型和动态值都为
nil
,此时为nil
接口值 ,调用其方法会导致程序崩溃 。

- 将
*os.File
类型的os.Stdout
赋给io.Writer
接口变量w
,会隐式转换 ,动态类型设为*os.File
,动态值为os.Stdout
副本 ,调用w.Write
实际调用(*os.File).Write
。

-
将
*bytes.Buffer
类型的new(bytes.Buffer)
赋给w
,动态类型变为*bytes.Buffer
,动态值为新分配缓冲区指针 ,调用w.Write
会追加内容到缓冲区 。 -
再将
nil
赋给w
,动态类型和动态值又变回nil
。 -
比较 :接口值可使用
==
和!=
操作符比较 ,两个接口值nil
或动态类型完全一致且动态值相等时相等 。但当动态类型一致 ,动态值不可比较(如 slice )时 ,比较会导致崩溃 。接口值可作为map
的键和switch
语句操作数 ,但需注意动态值的可比较性 。 -
获取动态类型 :处理错误或调试时 ,可使用
fmt
包的%T
格式化动词获取接口值的动态类型 ,其内部通过反射实现 。
注意:含有空指针的非空接口
空接口值不包含任何信息 ,动态类型和动态值均为
nil
;而仅动态值为nil
,动态类型非nil
的接口值 ,是含有空指针的非空接口 ,二者存在微妙区别 ,容易造成编程陷阱 。
go
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
为true
时 ,主函数创建*bytes.Buffer
指针buf
,并在debug
为true
时初始化为new(bytes.Buffer)
,然后调用函数f
,f
接收io.Writer
接口类型参数out
,在out
非nil
时向其写入数据 。- 分析 :当
debug
为false
时 ,buf
为nil
,将其传给f
,此时out
的动态类型是*bytes.Buffer
,动态值为nil
,是含有空指针的非空接口 。调用out.Write
时 ,由于*bytes.Buffer
类型的Write
方法要求接收者非空 ,会导致程序崩溃 。这是因为虽指针拥有的方法满足接口 ,但违背了方法隐式前置条件 。
go
var buf io.Writer if debug { buf = new(bytes.Buffer) // 启用输出收集 } f(buf) // OK
- 解决 :将
main
函数中buf
类型修改为io.Writer
,这样在debug
为false
时 ,buf
为nil
,符合io.Writer
接口的零值状态 ,调用f
时可避免将功能不完整的值传给接口 ,防止程序崩溃 。
使用 sort.Interface 来排序
go
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j 是序列元素的下标
Swap(i, j int)
}
- 作用 :
sort
包提供通用排序功能 ,sort.Interface
接口用于指定通用排序算法与具体序列类型间的协议 ,使排序算法不依赖序列和元素具体布局 ,实现灵活排序 。 - 定义 :该接口有
Len
(返回序列长度 )、Less
(比较元素大小 ,返回bool
)、Swap
(交换元素 )三个方法 。
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]
}
sort.Sort(StringSlice(names))
- 示例 :定义
StringSlice
类型 ,实现sort.Interface
接口的三个方法 ,通过sort.Sort(StringSlice(names))
可对字符串切片names
排序 。sort
包还提供StringSlice
类型和Strings
函数 ,简化为sort.Strings(names)
,且这种技术可复用 ,添加额外逻辑实现不同排序方式 。
复杂数据结构排序示例
go
type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
var tracks = []*Track{
{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
}
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\n"
tw := tabwriter.NewWriter(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() // 计算各列宽度并输出表格
}
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]
}
sort.Sort(byArtist(tracks))
type reverse struct{ i interface{} }
func (r reverse) Less(i, j int) bool {
return r.i.(Interface).Less(j, i)
}
func Reverse(data Interface) Interface {
return reverse{data}
}
sort.Reverse(byArtist(tracks))
- 音乐播放列表排序 :定义
Track
结构体表示音乐曲目 ,tracks
为*Track
指针切片 。要按Artist
字段排序 ,定义byArtist
类型实现sort.Interface
接口 ,通过sort.Sort(byArtist(tracks))
排序 。若需反向排序 ,使用sort.Reverse
函数 ,其内部基于嵌入sort.Interface
的reverse
类型实现 。
go
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]
}
sort.Sort(byYear(tracks))
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]
}
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
}})
func IntsAreSorted(values []int) bool {
for i := 1; i < len(values); i++ {
if values[i] < values[i-1] {
return false
}
}
return true
}
- 按其他字段排序 :如按
Year
字段排序 ,定义byYear
类型实现接口方法 ,调用sort.Sort(byYear(tracks))
。customSort
结构体类型 ,组合slice
和函数 ,只需定义比较函数就能实现新排序 。
http.Handler 接口
go
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
http.Handler
接口定义了ServeHTTP
方法 ,接收http.ResponseWriter
和*http.Request
作为参数 。ListenAndServe
函数用于启动服务器 ,接收服务器地址和Handler
接口实例 ,持续运行处理请求 ,直到出错。
error 接口
go
type error interface {
Error() string
}
error
是一个接口类型 ,定义了Error()
方法 ,返回类型为string
,用于返回错误消息 。
创建error
实例的方法
go
package errors
func New(text string) error { return &errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }
errors.New
函数 :构造error
最简单的方式 ,传入指定错误消息 ,返回包含该消息的error
实例 。error
包中New
函数通过创建errorString
结构体指针实现 ,这样可避免布局变更问题 ,且保证每次创建的error
实例不相等 。
go
func Errorf(format string, args...interface{}) error {
return errors.New(Sprintf(format, args...))
}
fmt.Errorf
函数 :更常用的封装函数 ,除创建error
实例外 ,还提供字符串格式化功能 ,其内部调用errors.New
。
不同的error
类型实现
errorString
类型 :满足error
接口的最基本类型 ,通过结构体指针实现 。syscall.Errno
类型 :syscall
包定义的数字类型 ,在 UNIX 平台上 ,其Error
方法从字符串表格中查询错误消息 ,是系统调用错误的高效表示 ,也满足error
接口 。
类型断言
类型断言是作用于接口值的操作 ,形式为x.(T)
,x
是接口类型表达式 ,T
是断言类型 ,用于检查操作数的动态类型是否满足指定断言类型 。
go
var w io.Writer
w = os.Stdout
f := w.(*os.File) // 成功: f == os.Stdout
c := w.(*bytes.Buffer) // 崩溃: 接口持有的是 *os.File,不是 *bytes.Buffer
- 断言类型为具体类型 :若
T
是具体类型 ,类型断言检查x
的动态类型是否为T
。检查成功 ,结果为x
的动态值 ,类型为T
;检查失败 ,操作崩溃 。如w.(*os.File)
,当w
动态类型是*os.File
时成功 ,否则崩溃 。
go
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // 成功: *os.File 有 Read 和 Write 方法
w = new(ByteCounter)
rw = w.(io.ReadWriter) // 崩溃: *ByteCounter 没有 Read 方法
- 断言类型为接口类型 :若
T
是接口类型 ,检查x
的动态类型是否满足T
。成功则结果仍是接口值 ,动态类型和值不变 ,但接口方法集可能变化 。如w.(io.ReadWriter)
,若w
动态类型满足该接口 ,可获取更多方法 。
go
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
- 空接口值 :操作数为空接口值时 ,类型断言失败 。一般很少从接口类型向更宽松类型做断言 ,多数情况与赋值一致 ,仅在操作
nil
时有区别 。
go
if w, ok := w.(*os.File); ok {
//...use w...
}
- 双返回值形式 :在需检测断言是否成功的场景 ,类型断言可返回两个值 ,第二个布尔值表示断言是否成功 。如
f, ok := w.(*os.File)
,ok
为true
时 ,f
为断言成功后的值 ,否则f
为断言类型零值 ,常用于if
表达式中进行条件操作 。 当操作数是变量时 ,可能出现返回值覆盖原有值的情况 。
使用类型断言来识别错误
go
package os
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
func IsNotExist(err error) bool {
// 注意: 不健壮
return strings.Contains(err.Error(), "file does not exist")
}
os
包中文件操作返回的错误通常因文件已存储、文件没找到、权限不足三类原因产生 。os
包提供IsExist
、IsNotExist
、IsPermission
等函数对错误分类 。简单通过检查错误消息字符串判断错误的方法不可靠 ,因不同平台错误消息可能不同 ,在生产级代码中不够健壮 。
结构化错误类型
go
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()
}
os
包定义PathError
类型表示与文件路径相关操作的错误 ,包含Op
(操作 )、Path
(路径 )、Err
(底层错误 )字段 ,还有类似的LinkError
。PathError
的Error
方法拼接字段返回错误字符串 ,其结构保留底层信息 。很多客户端忽略PathError
,直接调用Error
方法处理错误 ,但对于需区分错误的客户端 ,可使用类型断言检查错误类型 。
在错误识别中的应用
go
var ErrNotExist = errors.New("file does 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.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
// 实际使用
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"
以IsNotExist
函数为例 ,通过类型断言err.(*PathError)
判断错误是否为PathError
类型 ,若成功 ,进一步判断底层错误是否为syscall.ENOENT
或等于自定义的ErrNotExist
,以此确定错误是否代表文件或目录不存在 ,展示了类型断言在准确识别错误类型中的作用 。 错误识别应在失败操作发生时处理 ,避免错误信息合并导致结构信息丢失 。
通过接口类型断言来查询特性
性能优化场景引入
go
func writeHeader(w io.Writer, contentType string) error {
if _, err := w.Write([]byte("Content-Type: ")); err!= nil {
return err
}
if _, err := w.Write([]byte(contentType)); err!= nil {
return err
}
//...
}
// writeString 将 s 写入 w
// 如果 w 有 WriteString 方法,那么将直接调用该方法
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)) // 分配了临时内存
}
func writeHeader(w io.Writer, contentType string) error {
if _, err := writeString(w, "Content-Type: "); err!= nil {
return err
}
if _, err := writeString(w, contentType); err!= nil {
return err
}
//...
}
在类似 Web 服务器向客户端响应 HTTP 头字段的场景中 ,io.Writer
用于写入响应内容 。因Write
方法需字节切片 ,将字符串转换为字节切片会有内存分配和复制开销 ,影响性能 。而很多实现io.Writer
的类型(如*bytes.Buffer
、*os.File
等 )有WriteString
方法 ,可避免临时内存分配 。
利用接口类型断言优化
go
interface {
io.Writer
WriteString(s string) (n int, err error)
}
定义stringWriter
接口 ,仅包含WriteString
方法 。通过类型断言w.(stringWriter)
判断io.Writer
接口变量w
的动态类型是否满足该接口 。若满足 ,直接调用WriteString
方法避免内存复制 ;若不满足 ,再使用Write
方法 。将检查逻辑封装在writeString
工具函数 ,并在writeHeader
等函数中调用 ,避免代码重复 。
接口特性约定与应用拓展
这种方式依赖于一种隐式约定 ,即若类型满足特定接口 ,其WriteString
方法与Write([]byte(s))
等效 。此技术不仅适用于io
包相关接口 ,在fmt.Printf
内部 ,也通过类型断言从通用类型interface{}
中识别error
或fmt.Stringer
接口 ,确定格式化方法 ,若不满足则用反射处理其他类型 。
类型分支
接口的两种风格
- 第一种风格 :像
io.Reader
、io.Writer
等接口 ,突出满足接口的具体类型之间的相似性 ,隐藏具体类型的布局和特有功能 ,强调接口方法 。 - 第二种风格:将接口作为具体类型的联合 ,利用接口值容纳多种具体类型的能力 ,运行时通过类型断言区分类型并处理 ,强调具体类型 ,不注重信息隐藏 ,这种风格称为可识别联合 。、
go
import "database/sql"
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)
//...
}
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) // (not shown)
} else {
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}
// 优化
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))
}
}
示例 :以数据库 SQL 查询 API 为例 ,sqlQuote
函数将参数值转为 SQL 字面量 ,原代码使用一系列类型断言的if - else
语句 ,可通过类型分支(type switch
)简化 。类型分支语句switch x.(type)
,操作数为接口值 ,分支基于接口值的动态类型判定 ,nil
分支需x == nil
,default
分支在其他分支不满足时执行 ,且不允许使用fallthrough
。类型分支还有扩展形式switch x := x.(type)
,能将提取的原始值绑定到新变量 ,使代码更清晰 ,如改写后的sqlQuote
函数 。 类型分支可方便处理多种类型 ,但传入类型不匹配时会崩溃 。
一些建议
避免不必要的接口抽象
新手设计新包时 ,常先创建大量接口 ,再定义其具体实现 ,但当接口只有一个实现时 ,这种抽象多余且有运行时成本 。可利用导出机制控制类型的方法或字段对外可见性 ,仅在有多个具体类型需按统一方式处理时才使用接口 。
- 特例:若接口和类型实现因依赖关系不能在同一包 ,即便接口只有一个具体实现 ,也可用接口解耦不同包 。
- 原则 :接口因抽象多个类型实现细节而存在 ,好的接口设计应简单 ,方法少 ,如
io.Writer
、fmt.Stringer
。设计新类型时 ,越小的接口越易满足 ,建议仅定义必要的接口 。
参考资料:《Go程序设计语言》