当前位置: 首页 > article >正文

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程序设计语言》

相关文章:

Go:接口

接口既约定 Go 语言中接口是抽象类型 &#xff0c;与具体类型不同 &#xff0c;不暴露数据布局、内部结构及基本操作 &#xff0c;仅提供一些方法 &#xff0c;拿到接口类型的值 &#xff0c;只能知道它能做什么 &#xff0c;即提供了哪些方法 。 func Fprintf(w io.Writer, …...

ESP32+Arduino入门(三):连接WIFI获取当前时间

ESP32内置了WIFI模块连接WIFI非常简单方便。 代码如下&#xff1a; #include <WiFi.h>const char* ssid "WIFI名称"; const char* password "WIFI密码";void setup() {Serial.begin(115200);WiFi.begin(ssid,password);while(WiFi.status() ! WL…...

FastAPI用户认证系统开发指南:从零构建安全API

前言 在现代Web应用开发中&#xff0c;用户认证系统是必不可少的功能。本文将带你使用FastAPI框架构建一个完整的用户认证系统&#xff0c;包含注册、登录、信息更新和删除等功能。我们将采用JWT&#xff08;JSON Web Token&#xff09;进行身份验证&#xff0c;并使用SQLite作…...

CSS高度坍塌?如何解决?

一、什么是高度坍塌&#xff1f; 高度坍塌&#xff08;Collapsing Margins&#xff09;是指当父元素没有设置边框&#xff08;border&#xff09;、内边距&#xff08;padding&#xff09;、内容&#xff08;content&#xff09;或清除浮动时&#xff0c;其子元素的 margin 会…...

【数据结构】之散列

一、定义与基本术语 &#xff08;一&#xff09;、定义 散列&#xff08;Hash&#xff09;是一种将键&#xff08;key&#xff09;通过散列函数映射到一个固定大小的数组中的技术&#xff0c;因为键值对的映射关系&#xff0c;散列表可以实现快速的插入、删除和查找操作。在这…...

空地机器人在复杂动态环境下,如何高效自主导航?

随着空陆两栖机器人(AGR)在应急救援和城市巡检等领域的应用范围不断扩大&#xff0c;其在复杂动态环境中实现自主导航的挑战也日益凸显。对此香港大学王俊铭基于阿木实验室P600无人机平台自主搭建了一整套空地两栖机器人&#xff0c;使用Prometheus开源框架完成算法的仿真验证与…...

python小记(十二):Python 中 Lambda函数详解

Python 中 Lambda函数详解 Lambda函数详解&#xff1a;从入门到实战一、什么是Lambda函数&#xff1f;二、Lambda的核心语法与特点1. 基础语法2. 与普通函数对比 三、Lambda的六大应用场景&#xff08;附代码示例&#xff09;1. 基本数学运算2. 列表排序与自定义规则3. 数据映射…...

第二十一讲 XGBoost 回归建模 + SHAP 可解释性分析(利用R语言内置数据集)

下面我将使用 R 语言内置的 mtcars 数据集&#xff0c;模拟一个完整的 XGBoost 回归建模 SHAP 可解释性分析 实战流程。我们将以预测汽车的油耗&#xff08;mpg&#xff09;为目标变量&#xff0c;构建 XGBoost 模型&#xff0c;并用 SHAP 来解释模型输出。 &#x1f697; 示例…...

数据分析实战案例:使用 Pandas 和 Matplotlib 进行居民用水

原创 IT小本本 IT小本本 2025年04月15日 18:31 北京 本文将使用 Matplotlib 及 Seaborn 进行数据可视化。探索如何清理数据、计算月度用水量并生成有价值的统计图表&#xff0c;以便更好地理解居民的用水情况。 数据处理与清理 读取 Excel 文件 首先&#xff0c;我们使用 pan…...

Asp.NET Core WebApi 创建带鉴权机制的Api

构建一个包含 JWT&#xff08;JSON Web Token&#xff09;鉴权的 Web API 是一种常见的做法&#xff0c;用于保护 API 端点并验证用户身份。以下是一个基于 ASP.NET Core 的完整示例&#xff0c;展示如何实现 JWT 鉴权。 1. 创建 ASP.NET Core Web API 项目 使用 .NET CLI 或 …...

hash.

Redis 自身就是键值对结构 Redis 自身的键值对结构就是通过 哈希 的方式来组织的 哈希类型中的映射关系通常称为 field-value&#xff0c;用于区分 Redis 整体的键值对&#xff08;key-value&#xff09;&#xff0c; 注意这里的 value 是指 field 对应的值&#xff0c;不是键…...

记录鸿蒙应用上架应用未配置图标的前景图和后景图标准要求尺寸1024px*1024px和标准要求尺寸1024px*1024px

审核报错【①应用未配置图标的前景图和后景图,标准要求尺寸1024px*1024px且需下载HUAWEI DevEco Studio 5.0.5.315或以上版本进行图标再处理、②应用在展开状态下存在页面左边距过大的问题, 应用在展开状态下存在页面右边距过大的问题, 当前页面左边距: 504 px, 当前页面右边距…...

golang-常见的语法错误

https://juejin.cn/post/6923477800041054221 看这篇文章 Golang 基础面试高频题详细解析【第一版】来啦&#xff5e; 大叔说码 for-range的坑 func main() { slice : []int{0, 1, 2, 3} m : make(map[int]*int) for key, val : range slice {m[key] &val }for k, v : …...

Google最新《Prompt Engineering》白皮书全解析

近期有幸拿到了Google最新发布的《Prompt Engineering》白皮书&#xff0c;这是一份由Lee Boonstra主笔&#xff0c;Michael Sherman、Yuan Cao、Erick Armbrust、Antonio Gulli等多位专家共同贡献的权威性指南&#xff0c;发布于2025年2月。今天我想和大家分享这份68页的宝贵资…...

如何快速部署基于Docker 的 OBDIAG 开发环境

很多开发者对 OceanBase的 SIG社区小组很有兴趣&#xff0c;但如何将OceanBase的各类工具部署在开发环境&#xff0c;对于不少开发者而言都是比较蛮烦的事情。例如&#xff0c;像OBDIAG&#xff0c;其在WINDOWS系统上配置较繁琐&#xff0c;需要单独搭建C开发环境。此外&#x…...

[LeetCode 1306] 跳跃游戏3(Ⅲ)

题面&#xff1a; LeetCode 1306 思路&#xff1a; 只要能跳到其中一个0即可&#xff0c;和跳跃游戏1/2完全不同了&#xff0c;记忆化暴搜即可。 时间复杂度&#xff1a; O ( n ) O(n) O(n) 空间复杂度&#xff1a; O ( n ) O(n) O(n) 代码&#xff1a; dfs vector<…...

spring-ai-alibaba使用Agent实现智能机票助手

示例目标是使用 Spring AI Alibaba 框架开发一个智能机票助手&#xff0c;它可以帮助消费者完成机票预定、问题解答、机票改签、取消等动作&#xff0c;具体要求为&#xff1a; 基于 AI 大模型与用户对话&#xff0c;理解用户自然语言表达的需求支持多轮连续对话&#xff0c;能…...

STM32平衡车开发实战教程:从零基础到项目精通

STM32平衡车开发实战教程&#xff1a;从零基础到项目精通 一、项目概述与基本原理 1.1 平衡车工作原理 平衡车是一种基于倒立摆原理的两轮自平衡小车&#xff0c;其核心控制原理类似于人类保持平衡的过程。当人站立不稳时&#xff0c;会通过腿部肌肉的快速调整来维持平衡。平…...

使用DeepSeek AI高效降低论文重复率

一、论文查重原理与DeepSeek降重机制 1.1 主流查重系统工作原理 文本比对算法:连续字符匹配(通常13-15字符)语义识别技术:检测同义替换和结构调整参考文献识别:区分合理引用与不当抄袭跨语言检测:中英文互译内容识别1.2 DeepSeek降重核心技术 深度语义理解:分析句子核心…...

linux多线(进)程编程——(7)消息队列

前言 现在修真界大家的沟通手段已经越来越丰富了&#xff0c;有了匿名管道&#xff0c;命名管道&#xff0c;共享内存等多种方式。但是随着深入使用人们逐渐发现了这些传音术的局限性。 匿名管道&#xff1a;只能在有血缘关系的修真者&#xff08;进程&#xff09;间使用&…...

WinForm真入门(14)——ListView控件详解

一、ListView 控件核心概念与功能 ‌ListView‌ 是 WinForm 中用于展示结构化数据的多功能列表控件&#xff0c;支持多列、多视图模式及复杂交互&#xff0c;常用于文件资源管理器、数据报表等场景‌。 核心特点‌&#xff1a; 支持 ‌5种视图模式‌&#xff1a;Details&…...

Python + Playwright:规避常见的UI自动化测试反模式

Python + Playwright:规避常见的UI自动化测试反模式 前言反模式一:整体式页面对象(POM)反模式二:具有逻辑的页面对象 - POM 的“越界”行为反模式三:基于 UI 的测试设置 - 缓慢且脆弱的“舞台搭建”反模式四:功能测试过载 - “试图覆盖一切”的测试反模式之间的关联与核…...

从服务器多线程批量下载文件到本地

1、客户端安装 aria2 下载地址&#xff1a;aria2 解压文件&#xff0c;然后将文件目录添加到系统环境变量Path中&#xff0c;然后打开cmd&#xff0c;输入&#xff1a;aria2c 文件地址&#xff0c;就可以下载文件了 2、服务端配置nginx文件服务器 server {listen 8080…...

循环神经网络 - 深层循环神经网络

如果将深度定义为网络中信息传递路径长度的话&#xff0c;循环神经网络可以看作既“深”又“浅”的网络。 一方面来说&#xff0c;如果我们把循环网络按时间展开&#xff0c;长时间间隔的状态之间的路径很长&#xff0c;循环网络可以看作一个非常深的网络。 从另一方面来 说&…...

linux运维篇-Ubuntu(debian)系操作系统创建源仓库

适用范围 适用于Ubuntu&#xff08;Debian&#xff09;及其衍生版本的linux系统 例如&#xff0c;国产化操作系统kylin-desktop-v10 简介 先来看下我们需要创建出来的仓库目录结构 Deb_conf_test apt源的主目录 conf 配置文件存放目录 conf目录下存放两个配置文件&…...

深度学习之微积分

2.4.1 导数和微分 2.4.2 偏导数 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/17227e00adb14472902baba4da675aed.png 2.4.3 梯度 具体证明&#xff0c;矩阵-向量积...

20242817李臻《Linux⾼级编程实践》第7周

20242817李臻《Linux⾼级编程实践》第7周 一、AI对学习内容的总结 第八章&#xff1a;多线程编程 8.1 多线程概念 进程与线程的区别&#xff1a; 进程是资源分配单位&#xff0c;拥有独立的地址空间、全局变量、打开的文件等。线程是调度单位&#xff0c;在同一进程内的线程…...

浙江大学:DeepSeek如何引领智慧医疗的革新之路?|48页PPT下载方法

导 读INTRODUCTION 随着人工智能技术的飞速发展&#xff0c;DeepSeek等大模型正在引领医疗行业进入一个全新的智慧医疗时代。这些先进的技术不仅正在改变医疗服务的提供方式&#xff0c;还在提高医疗质量和效率方面展现出巨大潜力。 想象一下&#xff0c;当你走进医院&#xff…...

Android基础彻底解析-APK入口点,xml,组件,脱壳,逆向

第一章:引言与背景 Android逆向工程,作为一种深入分析Android应用程序的技术,主要目的就是通过分析应用的代码、资源和行为来理解其功能、结构和潜在的安全问题。它不仅仅是对应用进行破解或修改,更重要的是帮助开发者、研究人员和安全人员发现并解决安全隐患。 本文主要对…...

ubuntu 2204 安装 vcs 2018

安装评估 系统 : Ubuntu 22.04.1 LTS 磁盘 : ubuntu 自身占用了 9.9G , 按照如下步骤 安装后 , 安装后的软件 占用 13.1G 仓库 : 由于安装 libpng12-0 , 添加了一个仓库 安装包 : 安装了多个包(lsb及其依赖包 libpng12-0)安装步骤 参考 ubuntu2018 安装 vcs2018 安装该…...