目录

Go:接口

接口既约定

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接口由ReaderWriter接口组合而成 ,ReadWriteCloser接口由ReaderWriterCloser接口组合而成 ,这种方式称为嵌入式接口 ,类似嵌入式结构 ,可直接使用组合后的接口 ,无需逐一写出其包含的方法 。
  • 三种声明效果一致 ,方法定义顺序无意义 ,关键是接口的方法集合 。

实现接口

  • 具体类型实现接口需实现接口定义的所有方法 ,如*os.File实现了io.ReaderWriterCloserReaderWriter接口 ,*bytes.Buffer实现了ReaderWriterReaderWriter接口 。
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.Printlnerrorf等函数能接受任意类型参数就是利用了空接口 。但不能直接使用空接口值 ,需通过类型断言等方法还原实际值 。

隐式声明

  • 编译器可隐式判断类型实现接口 ,如*bytes.Buffer的任意值(包括nil )都实现了io.Writer接口 ,可简化变量声明 。非空接口常由指针类型实现 ,但也有其他引用类型可实现接口 ,如slicemap 、函数类型等 ,且基本类型也能通过定义方法实现接口 ,如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
}
  • 接口用于抽象分组 :以管理或销售数字文化商品的程序为例 ,定义多种具体类型(如AlbumBook等 ) ,可针对不同属性定义接口(如ArtifactsPagesAudioVideo ) ,还可进一步组合接口(如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 ,用户可用友好方式指定时长 ,如50ms2m30s等 ,程序进入睡眠前输出睡眠时长 。

go 复制代码
package flag

// Value 接口代表了存储在标志内的值
type Value interface {
    String() string
    Set(string) error
}

flag.Value接口定义了StringSet方法 。String方法用于格式化标志对应的值 ,输出命令行帮助消息 ,实现该接口的类型也是fmt.StringerSet方法用于解析传入的字符串参数并更新标志值 ,是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从输入字符串解析浮点值和单位 ,根据单位(CF )进行摄氏温度和华氏温度转换 ,若输入无效则返回错误 。
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"))
    }
}
  • 示例 :程序中当debugtrue时 ,主函数创建*bytes.Buffer指针buf ,并在debugtrue时初始化为new(bytes.Buffer) ,然后调用函数ff接收io.Writer接口类型参数out ,在outnil时向其写入数据 。
  • 分析 :当debugfalse时 ,bufnil ,将其传给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 ,这样在debugfalse时 ,bufnil ,符合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.Interfacereverse类型实现 。
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)oktrue时 ,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包提供IsExistIsNotExistIsPermission等函数对错误分类 。简单通过检查错误消息字符串判断错误的方法不可靠 ,因不同平台错误消息可能不同 ,在生产级代码中不够健壮 。

结构化错误类型

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(底层错误 )字段 ,还有类似的LinkErrorPathErrorError方法拼接字段返回错误字符串 ,其结构保留底层信息 。很多客户端忽略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{}中识别errorfmt.Stringer接口 ,确定格式化方法 ,若不满足则用反射处理其他类型 。

类型分支

接口的两种风格

  • 第一种风格 :像io.Readerio.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 == nildefault分支在其他分支不满足时执行 ,且不允许使用fallthrough 。类型分支还有扩展形式switch x := x.(type) ,能将提取的原始值绑定到新变量 ,使代码更清晰 ,如改写后的sqlQuote函数 。 类型分支可方便处理多种类型 ,但传入类型不匹配时会崩溃 。

一些建议

避免不必要的接口抽象

新手设计新包时 ,常先创建大量接口 ,再定义其具体实现 ,但当接口只有一个实现时 ,这种抽象多余且有运行时成本 。可利用导出机制控制类型的方法或字段对外可见性 ,仅在有多个具体类型需按统一方式处理时才使用接口 。

  • 特例:若接口和类型实现因依赖关系不能在同一包 ,即便接口只有一个具体实现 ,也可用接口解耦不同包 。
  • 原则 :接口因抽象多个类型实现细节而存在 ,好的接口设计应简单 ,方法少 ,如io.Writerfmt.Stringer 。设计新类型时 ,越小的接口越易满足 ,建议仅定义必要的接口 。

参考资料:《Go程序设计语言》

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
苏州向日葵4 分钟前
C#学习知识点记录
开发语言·学习·c#
残轩14 分钟前
Win10 家庭版 Docker 环境搭建详解(基于 WSL2)
前端·后端·docker
十分钟空间15 分钟前
后端涨薪密码:5 框架 + MCP 攻略,学会直接涨 30%
后端·flask·trae
努力学习的小廉15 分钟前
【C++】 —— 笔试刷题day_23
开发语言·c++
Danny8817 分钟前
🔐 前后端文件加密解密实战:从坑到方案全记录
后端
Piper蛋窝18 分钟前
Go 1.14 相比 Go 1.13 有哪些值得注意的改动?
后端
Andya23 分钟前
SpringBoot | 构建客户树及其关联关系的设计思路和实践Demo
后端
居然是阿宋25 分钟前
Kotlin函数体详解:表达式函数体 vs 代码块函数体——使用场景与最佳实践
java·开发语言·kotlin
GetcharZp25 分钟前
FileBrowser:用浏览器轻松管理服务器文件,简洁又强大
后端·go
小钊(求职中)28 分钟前
Java后端开发面试题(含答案)
java·开发语言·后端·面试