接口既約定
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.BufferFprintf(&buf, format, args...)return buf.String()
}
fmt.Fprintf
函數用于格式化輸出 ,其第一個形參是io.Writer
接口類型 。io.Writer
接口封裝了基礎寫入方法 ,定義了Write
方法 ,要求將數據寫入底層數據流 ,并返回實際寫入字節數等 。
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
能使用滿足該接口的參數 ,體現了可取代性 ,即只要滿足接口 ,具體類型可相互替換 。
type ByteCounter intfunc (c *ByteCounter) Write(p []byte) (int, error) {*c += ByteCounter(len(p))return len(p), nil
}
- 以
ByteCounter
類型為例 ,它實現了Write
方法 ,滿足io.Writer
接口約定 ,可在fmt.Fprintf
中使用 。
package fmt// 在字符串格式化時如果需要一個字符串
// 那么就調用這個方法來把當前值轉化為字符串
// Print 這種不帶格式化參數的輸出方式也是調用這個方法
type Stringer interface {String() string
}
fmt
包還有fmt.Stringer
接口 ,定義了String
方法 。類型實現該方法 ,就能在字符串格式化時將自身轉化為字符串輸出 ,如之前的Celsius
、*IntSet
類型添加String
方法后 ,可滿足該接口 ,實現特定格式輸出 。
接口類型
接口類型定義了一套方法 ,具體類型要實現某接口 ,必須實現該接口定義的所有方法 。如io.Writer
接口抽象了所有可寫入字節的類型 ,像文件、內存緩沖區等 ,具體類型若要符合io.Writer
,就得實現其Write
方法 。
package iotype Reader interface {Read(p []byte) (n int, err error)
}type Closer interface {Close() error
}
Reader
接口:抽象了所有可讀取字節的類型 ,定義了Read
方法 ,用于從類型中讀取數據 。Closer
接口:抽象了所有可關閉的類型 ,如文件、網絡連接等 ,定義了Close
方法 。
type ReadWriter interface {ReaderWriter
}type ReadWriteCloser interface {ReaderWriterCloser
}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
接口 。
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
接口方法 ,實現前者的類型也實現了后者 。
type IntSet struct { /*... */ }
func (*IntSet) String() stringvar _ = 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
接口 。
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
。
type Text interface {Pages() intWords() intPageSize() int
}type Audio interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string // 比如 "MP3"、"WAV"
}type Video interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string // 比如 "MP4"、"WMV"Resolution() (x, y int)
}type Streamer interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string
}
- 接口用于抽象分組:以管理或銷售數字文化商品的程序為例 ,定義多種具體類型(如
Album
、Book
等 ) ,可針對不同屬性定義接口(如Artifacts
、Pages
、Audio
、Video
) ,還可進一步組合接口(如Streamer
) 。Go 語言可按需定義抽象和分組 ,不用修改原有類型定義 ,從具體類型提取共性用接口表示 。
使用 flag.Value 來解析參數
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
等 ,程序進入睡眠前輸出睡眠時長 。
package flag// Value 接口代表了存儲在標志內的值
type Value interface {String() stringSet(string) error
}
flag.Value
接口定義了String
和Set
方法 。String
方法用于格式化標志對應的值 ,輸出命令行幫助消息 ,實現該接口的類型也是fmt.Stringer
;Set
方法用于解析傳入的字符串參數并更新標志值 ,是String
方法的逆操作 。
自定義實現flag.Value
接口
// *celsiusFlag 滿足 flag.Value 接口
type celsiusFlag struct{ Celsius }func (f *celsiusFlag) Set(s string) error {var unit stringvar value float64fmt.Sscanf(s, "%f%s", &value, &unit) // 無須檢查錯誤switch 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
類型 ,內嵌Celsius
類型 ,因已有String
方法 ,只需實現Set
方法來滿足flag.Value
接口 。Set
方法中 ,使用fmt.Sscanf
從輸入字符串解析浮點值和單位 ,根據單位(C
或F
)進行攝氏溫度和華氏溫度轉換 ,若輸入無效則返回錯誤 。
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 語言中 ,類型是編譯時概念 ,不是值 ,接口值的類型部分用類型描述符表示 。
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
的接口值 ,是含有空指針的非空接口 ,二者存在微妙區別 ,容易造成編程陷阱 。const debug = truefunc main() {var buf *bytes.Bufferif 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
方法要求接收者非空 ,會導致程序崩潰 。這是因為雖指針擁有的方法滿足接口 ,但違背了方法隱式前置條件 。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 來排序
package sorttype Interface interface {Len() intLess(i, j int) bool // i, j 是序列元素的下標Swap(i, j int)
}
- 作用:
sort
包提供通用排序功能 ,sort.Interface
接口用于指定通用排序算法與具體序列類型間的協議 ,使排序算法不依賴序列和元素具體布局 ,實現靈活排序 。 - 定義:該接口有
Len
(返回序列長度 )、Less
(比較元素大小 ,返回bool
)、Swap
(交換元素 )三個方法 。
type StringSlice []stringfunc (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)
,且這種技術可復用 ,添加額外邏輯實現不同排序方式 。
復雜數據結構排序示例
type Track struct {Title stringArtist stringAlbum stringYear intLength 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 []*Trackfunc (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
類型實現 。
type byYear []*Trackfunc (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 []*Trackless 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 接口
package httptype Handler interface {ServeHTTP(w ResponseWriter, r *Request)
}func ListenAndServe(address string, h Handler) error
http.Handler
接口定義了ServeHTTP
方法 ,接收http.ResponseWriter
和*http.Request
作為參數 。ListenAndServe
函數用于啟動服務器 ,接收服務器地址和Handler
接口實例 ,持續運行處理請求 ,直到出錯。
error 接口
type error interface {Error() string
}
error
是一個接口類型 ,定義了Error()
方法 ,返回類型為string
,用于返回錯誤消息 。
創建error
實例的方法
package errorsfunc 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
實例不相等 。
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
是斷言類型 ,用于檢查操作數的動態類型是否滿足指定斷言類型 。
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
時成功 ,否則崩潰 。
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
動態類型滿足該接口 ,可獲取更多方法 。
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
時有區別 。
if w, ok := w.(*os.File); ok {//...use w...
}
- 雙返回值形式:在需檢測斷言是否成功的場景 ,類型斷言可返回兩個值 ,第二個布爾值表示斷言是否成功 。如
f, ok := w.(*os.File)
,ok
為true
時 ,f
為斷言成功后的值 ,否則f
為斷言類型零值 ,常用于if
表達式中進行條件操作 。 當操作數是變量時 ,可能出現返回值覆蓋原有值的情況 。
使用類型斷言來識別錯誤
package osfunc IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) boolfunc IsNotExist(err error) bool {// 注意: 不健壯return strings.Contains(err.Error(), "file does not exist")
}
os
包中文件操作返回的錯誤通常因文件已存儲、文件沒找到、權限不足三類原因產生 。os
包提供IsExist
、IsNotExist
、IsPermission
等函數對錯誤分類 。簡單通過檢查錯誤消息字符串判斷錯誤的方法不可靠 ,因不同平臺錯誤消息可能不同 ,在生產級代碼中不夠健壯 。
結構化錯誤類型
package os// PathError 記錄了錯誤以及錯誤相關的操作和文件路徑
type PathError struct {Op stringPath stringErr error
}func (e *PathError) Error() string {return e.Op + " " + e.Path + ": " + e.Err.Error()
}
os
包定義PathError
類型表示與文件路徑相關操作的錯誤 ,包含Op
(操作 )、Path
(路徑 )、Err
(底層錯誤 )字段 ,還有類似的LinkError
。PathError
的Error
方法拼接字段返回錯誤字符串 ,其結構保留底層信息 。很多客戶端忽略PathError
,直接調用Error
方法處理錯誤 ,但對于需區分錯誤的客戶端 ,可使用類型斷言檢查錯誤類型 。
在錯誤識別中的應用
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
,以此確定錯誤是否代表文件或目錄不存在 ,展示了類型斷言在準確識別錯誤類型中的作用 。 錯誤識別應在失敗操作發生時處理 ,避免錯誤信息合并導致結構信息丟失 。
通過接口類型斷言來查詢特性
性能優化場景引入
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
方法 ,可避免臨時內存分配 。
利用接口類型斷言優化
interface {io.WriterWriteString(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
等接口 ,突出滿足接口的具體類型之間的相似性 ,隱藏具體類型的布局和特有功能 ,強調接口方法 。 - 第二種風格:將接口作為具體類型的聯合 ,利用接口值容納多種具體類型的能力 ,運行時通過類型斷言區分類型并處理 ,強調具體類型 ,不注重信息隱藏 ,這種風格稱為可識別聯合 。、
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程序設計語言》