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.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接口由ReaderWriter接口組合而成 ,ReadWriteCloser接口由ReaderWriterCloser接口組合而成 ,這種方式稱為嵌入式接口 ,類似嵌入式結構 ,可直接使用組合后的接口 ,無需逐一寫出其包含的方法 。
  • 三種聲明效果一致 ,方法定義順序無意義 ,關鍵是接口的方法集合 。

實現接口

  • 具體類型實現接口需實現接口定義的所有方法 ,如*os.File實現了io.ReaderWriterCloserReaderWriter接口 ,*bytes.Buffer實現了ReaderWriterReaderWriter接口 。
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.Printlnerrorf等函數能接受任意類型參數就是利用了空接口 。但不能直接使用空接口值 ,需通過類型斷言等方法還原實際值 。

隱式聲明

  • 編譯器可隱式判斷類型實現接口 ,如*bytes.Buffer的任意值(包括nil )都實現了io.Writer接口 ,可簡化變量聲明 。非空接口常由指針類型實現 ,但也有其他引用類型可實現接口 ,如slicemap 、函數類型等 ,且基本類型也能通過定義方法實現接口 ,如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
}
  • 接口用于抽象分組:以管理或銷售數字文化商品的程序為例 ,定義多種具體類型(如AlbumBook等 ) ,可針對不同屬性定義接口(如ArtifactsPagesAudioVideo ) ,還可進一步組合接口(如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 ,用戶可用友好方式指定時長 ,如50ms2m30s等 ,程序進入睡眠前輸出睡眠時長 。

package flag// Value 接口代表了存儲在標志內的值
type Value interface {String() stringSet(string) error
}

flag.Value接口定義了StringSet方法 。String方法用于格式化標志對應的值 ,輸出命令行幫助消息 ,實現該接口的類型也是fmt.StringerSet方法用于解析傳入的字符串參數并更新標志值 ,是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從輸入字符串解析浮點值和單位 ,根據單位(CF )進行攝氏溫度和華氏溫度轉換 ,若輸入無效則返回錯誤 。
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接口值 ,調用其方法會導致程序崩潰 。

image.png

  • *os.File類型的os.Stdout賦給io.Writer接口變量w ,會隱式轉換 ,動態類型設為*os.File ,動態值為os.Stdout副本 ,調用w.Write實際調用(*os.File).Write

image.png

  • *bytes.Buffer類型的new(bytes.Buffer)賦給w ,動態類型變為*bytes.Buffer ,動態值為新分配緩沖區指針 ,調用w.Write會追加內容到緩沖區 。

  • 再將nil賦給w ,動態類型和動態值又變回nil

  • 比較:接口值可使用==!=操作符比較 ,兩個接口值nil或動態類型完全一致且動態值相等時相等 。但當動態類型一致 ,動態值不可比較(如 slice )時 ,比較會導致崩潰 。接口值可作為map的鍵和switch語句操作數 ,但需注意動態值的可比較性 。

  • 獲取動態類型:處理錯誤或調試時 ,可使用fmt包的%T格式化動詞獲取接口值的動態類型 ,其內部通過反射實現 。

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

image.png

空接口值不包含任何信息 ,動態類型和動態值均為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"))}
}
  • 示例:程序中當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方法要求接收者非空 ,會導致程序崩潰 。這是因為雖指針擁有的方法滿足接口 ,但違背了方法隱式前置條件 。
var buf io.Writer
if debug {buf = new(bytes.Buffer) // 啟用輸出收集
}
f(buf) // OK
  • 解決:將main函數中buf類型修改為io.Writer ,這樣在debugfalse時 ,bufnil ,符合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.Interfacereverse類型實現 。
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)oktrue時 ,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包提供IsExistIsNotExistIsPermission等函數對錯誤分類 。簡單通過檢查錯誤消息字符串判斷錯誤的方法不可靠 ,因不同平臺錯誤消息可能不同 ,在生產級代碼中不夠健壯 。

結構化錯誤類型

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(底層錯誤 )字段 ,還有類似的LinkErrorPathErrorError方法拼接字段返回錯誤字符串 ,其結構保留底層信息 。很多客戶端忽略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{}中識別errorfmt.Stringer接口 ,確定格式化方法 ,若不滿足則用反射處理其他類型 。

類型分支

接口的兩種風格

  • 第一種風格:像io.Readerio.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 == nildefault分支在其他分支不滿足時執行 ,且不允許使用fallthrough 。類型分支還有擴展形式switch x := x.(type) ,能將提取的原始值綁定到新變量 ,使代碼更清晰 ,如改寫后的sqlQuote函數 。 類型分支可方便處理多種類型 ,但傳入類型不匹配時會崩潰 。

一些建議

避免不必要的接口抽象

新手設計新包時 ,常先創建大量接口 ,再定義其具體實現 ,但當接口只有一個實現時 ,這種抽象多余且有運行時成本 。可利用導出機制控制類型的方法或字段對外可見性 ,僅在有多個具體類型需按統一方式處理時才使用接口 。

  • 特例:若接口和類型實現因依賴關系不能在同一包 ,即便接口只有一個具體實現 ,也可用接口解耦不同包 。
  • 原則:接口因抽象多個類型實現細節而存在 ,好的接口設計應簡單 ,方法少 ,如io.Writerfmt.Stringer 。設計新類型時 ,越小的接口越易滿足 ,建議僅定義必要的接口 。

參考資料:《Go程序設計語言》

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/76833.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/76833.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/76833.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一、Appium環境安裝

找了一圈操作手機的工具或軟件&#xff0c;踩了好多坑&#xff0c;最后決定用這個工具(影刀RPA手機用的也是這個)&#xff0c;目前最新的版本是v2.17.1&#xff0c;是基于nodejs環境的&#xff0c;有兩種方式&#xff0c;我只試了第一種方式&#xff0c;第二種方式應該是比較簡…

【玩轉全棧】—— Django 連接 vue3 保姆級教程,前后端分離式項目2025年4月最新!!!

本文基于之前的一個旅游網站&#xff0c;實現 Django 連接 vue3&#xff0c;使 vue3 能攜帶 CSRF Token 發送 axios 請求給后端&#xff0c;后端再響應數據給前端。想要源碼直接滑倒底部。 目錄 實現效果 解決跨域 獲取 csrf-token 什么是 csrf-token &#xff1f; CSRF攻擊的…

dify部署,ollama部署,拉取模型,創建ai聊天應用

dify下載安裝 dify1.0.1 windos安裝包百度云盤地址 通過網盤分享的文件&#xff1a;dify-1.0.1.zip 鏈接: 百度網盤 請輸入提取碼 提取碼: 1234 dify安裝包 linux安裝包百度云盤地址 通過網盤分享的文件&#xff1a;dify-1.0.1.tar.gz 鏈接: 百度網盤 請輸入提取碼 提取碼…

docx文檔轉為pdf文件響應前端

1、轉換文件&#xff08;docx~pdf&#xff09; 1.引入pom依賴 <dependency><groupId>com.aspose</groupId><artifactId>aspose-words</artifactId><version>20.12.0</version> </dependency>2.讀取docx文檔數據-轉換 // 初…

網絡安全中信息收集需要收集哪些信息了?匯總

目錄 1. 域名信息 2. IP地址與網絡信息 3. 備案與注冊信息 4. Web應用與中間件信息 5. 操作系統與服務器信息 6. 敏感文件與配置文件 7. 社交工程信息 8. 證書與加密信息 9. API與接口信息 10. 外部威脅情報 11. 歷史數據與緩存 常用工具與技術&#xff1a; 在網絡…

【鋰電池SOH預測】PSO-BP鋰電池健康狀態預測,鋰電池SOH預測(Matlab完整源碼和數據)

預測效果 基于PSO-BP算法的鋰電池健康狀態預測研究 一、引言 1.1 研究背景與意義 在當今社會&#xff0c;鋰電池憑借其高能量密度、長壽命及環境友好等特性&#xff0c;在現代能源系統中占據著舉足輕重的地位。從消費電子領域如智能手機、筆記本電腦&#xff0c;到動力領域中…

智能車攝像頭開源—9 動態權、模糊PID、速度決策、路徑優化

目錄 一、前言 二、動態權 1.概述 2.偏差值加動態權 三、模糊PID 四、速度決策 1.曲率計算 2.速度擬合 3.速度控制 五、路徑 六、國賽視頻 一、前言 在前中期通過識別直道、彎道等元素可進行加減速操作實現速度的控制&#xff0c;可進一步縮減一圈的運行速度&#xff…

過往記錄系列 篇五:市場黑天鵝事件歷史梳理

文章目錄 系列文章文章地址文章摘要文章預覽系列文章 過往記錄系列 篇一:牛市板塊輪動順序梳理 過往記錄系列 篇二:新年1月份(至春節前)行情歷史梳理 過往記錄系列 篇三:春節行情歷史梳理 過往記錄系列 篇四:年報月行情歷史梳理 文章地址 原文審核不通過(理由:“違反…

Mysql--基礎知識點--85.1--Innodb自適應哈希索引

1. 自適應哈希索引的用途 InnoDB 的自適應哈希索引&#xff08;Adaptive Hash Index, AHI&#xff09;是 MySQL 數據庫引擎中一項智能優化查詢性能的功能。其核心作用如下&#xff1a; 加速等值查詢 哈希索引通過哈希函數將鍵映射到固定位置&#xff0c;實現 O(1) 時間復雜度的…

SQL優化技術分享:從 321 秒到 0.2 秒的性能飛躍 —— 基于 PawSQL 的 TPCH 查詢優化實戰

在數據庫性能優化領域&#xff0c;TPC-H 測試集是一個經典的基準測試工具&#xff0c;常用于評估數據庫系統的查詢性能。本文將基于 TPCH 測試集中的第 20個查詢&#xff0c;結合 PawSQL 自動化優化工具&#xff0c;詳細分析如何通過 SQL 重寫和索引設計&#xff0c;將查詢性能…

SpringBoot3-web開發筆記(下)

內容協商 實現&#xff1a;一套系統適配多端數據返回 多端內容適配&#xff1a; 1. 默認規則 SpringBoot 多端內容適配。 基于請求頭內容協商&#xff1a;&#xff08;默認開啟&#xff09; 客戶端向服務端發送請求&#xff0c;攜帶HTTP標準的Accept請求頭。 Accept: applica…

Graylog 索引配置詳解與優化建議

Graylog 索引配置詳解與優化建議 &#x1f680; 前言一、索引集基礎信息 &#x1f4da;二、分片&#xff08;Shards&#xff09;與副本&#xff08;Replicas&#xff09;設置 ??1. 分片 (Shards)2. 副本 (Replicas) 三、 字段類型刷新間隔&#xff08;Field Type Refresh Int…

數據結構*包裝類泛型

包裝類 什么是包裝類 在講基本數據類型的時候&#xff0c;有提到過包裝類。 基本數據類型包裝類byteByteshortShortintIntegerlongLongfloatFloatdoubleDoublecharCharacterbooleanBoolean 我們知道&#xff1a;基本數據類型并不是對象&#xff0c;沒有對象所具有的方法和屬…

【JDBC-54.1】MySQL JDBC連接字符串常用參數詳解

在Java應用程序中連接MySQL數據庫時&#xff0c;JDBC連接字符串是建立連接的關鍵。一個配置得當的連接字符串不僅能確保連接成功&#xff0c;還能優化性能、增強安全性并處理各種連接場景。本文將深入探討MySQL JDBC連接字符串的常用參數及其最佳實踐。 1. 基本連接字符串格式…

[ctfshow web入門] web37

信息收集 題目有了變化&#xff0c;include$c if(isset($_GET[c])){$c $_GET[c];if(!preg_match("/flag/i", $c)){include($c);echo $flag;}}else{highlight_file(__FILE__); }解題 通過協議解題 參考[ctfshow web入門] web31 同樣是include&#xff0c;之前的方…

Linux 調試代碼工具:gdb

文章目錄 一、debug vs release&#xff1a;兩種程序形態的本質差異1. 什么是 debug 與 release&#xff1f;2. 核心差異對比 二、為什么需要 debug&#xff1a;從項目生命周期看調試價值1. 項目開發流程中的調試閉環&#xff08;流程圖示意&#xff09;2. Debug 的核心意義與目…

Python設計模式:命令模式

1. 什么是命令模式&#xff1f; 命令模式是一種行為設計模式&#xff0c;它將請求封裝為一個對象&#xff0c;從而使您能夠使用不同的請求、隊列或日志請求&#xff0c;以及支持可撤銷操作。 命令模式的核心思想是將請求的發送者與請求的接收者解耦&#xff0c;使得兩者之間的…

nlp面試重點

深度學習基本原理&#xff1a;梯度下降公式&#xff0c;將損失函數越來越小&#xff0c;最終預測值和實際值誤差比較小。 交叉熵&#xff1a;-p(x)logq(x)&#xff0c;p(x)是one-hot形式。如果不使用softmax計算交叉熵&#xff0c;是不行的。損失函數可能會非常大&#xff0c;…

Leetcode:二叉樹

94. 二叉樹的中序遍歷 class Solution {public List<Integer> inorderTraversal(TreeNode root) {TreeNode cur root;Stack<TreeNode> stack new Stack<>();List<Integer> list new ArrayList<>();while (!stack.isEmpty() || cur ! null) {…

SQL:Constraint(約束)

目錄 &#x1f3af; 什么是 Constraint&#xff1f; MySQL 中常見的約束類型&#xff1a; 1. PRIMARY KEY 2. FOREIGN KEY 3. UNIQUE 4. NOT NULL 5. DEFAULT 6. CHECK&#xff08;MySQL 8.0&#xff09; 7. AUTO_INCREMENT &#x1f3af; 什么是 Constraint&#xf…