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

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, …...

js | 网页上的 json 数据怎么保存到本地表格中?

1.思路 json 转为 csv 保存到本地或者&#xff1a;json 转为 html 显示到网页中&#xff0c;然后复制到excel中。 (2) 数据 wjl{"code":1,"data":[{"chrmiRNA":"chr1","0":"chr1","startmiRNA":&quo…...

智能Todo协作系统开发日志(二):架构优化与安全增强

&#x1f4c5; 2025年4月14日 | 作者&#xff1a;Aphelios380 &#x1f31f; 今日优化目标 在原Todo单机版基础上进行三大核心升级&#xff1a; 组件化架构改造 - 提升代码可维护性 本地数据加密存储 - 增强隐私安全性 无障碍访问支持 - 践行W3C标准 一、组件化架构改造 …...

buctoj_算法设计与分析(5)

问题 A: 没有上司的舞会 题目描述 Ural大学有N名职员&#xff0c;编号为1~N。 他们的关系就像一棵以校长为根的树&#xff0c;父节点就是子节点的直接上司。 每个职员有一个快乐指数&#xff0c;用整数 HiHi 给出&#xff0c;其中 1≤i≤N。 现在要召开一场周年庆宴会&#x…...

VUE项目中的package.json中的启动脚本

"scripts": {"dev": "vite","build:prod": "vite build","build:stage": "vite build --mode staging","preview": "vite preview"}vite build 和 vite build --mode staging 是 V…...

目标追踪数据标注

在将 YOLO&#xff08;目标检测&#xff09; 和 DeepSORT&#xff08;目标追踪&#xff09; 结合时&#xff0c;数据标注需要同时满足 检测 和 追踪 的需求。以下是具体的分阶段标注策略和操作指南&#xff1a; 一、标注的核心要求 检测标注&#xff1a;每帧中目标的 边界框&a…...

详细介绍7大排序算法

1.排序的概念及其运用 1.1 排序的概念 排序 &#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性 &#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记…...

TGCTF web

AAA偷渡阴平 这个题是一个非预期的无参RCE <?php$tgctf2025$_GET[tgctf2025];if(!preg_match("/0|1|[3-9]|\~|\|\|\#|\\$|\%|\^|\&|\*|\&#xff08;|\&#xff09;|\-|\|\|\{|\[|\]|\}|\:|\|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){//hi…...

RTPS数据包分析

DDS-RTPS 常见子消息_dds pdp消息-CSDN博客Fast RTPS原理与代码分析(3)&#xff1a;动态发现协议之端点发现协议EDP_fast-rtps 原理-CSDN博客 在RTPS&#xff08;Real-Time Publish-Subscribe&#xff0c;实时发布订阅&#xff09;协议中&#xff0c;DATA(r)和DATA是两种不同的…...

go语言gRPC使用流程

1. 安装工具和依赖 安装 Protocol Buffers 编译器 (protoc) 下载地址&#xff1a;https://github.com/protocolbuffers/protobuf/releases 使用说明&#xff1a;https://protobuf.dev/ 【centos环境】yum方式安装&#xff1a;protoc[rootlocalhost demo-first]# yum install …...

回溯算法的要点

可以用树结构&#xff08;解空间树&#xff09;来表示用回溯法解决的问题的所有选项。 叶节点则对应着最终的状态. 回溯过程&#xff1a;深度遍历&#xff0c;在任意时刻&#xff0c;算法只保存从根结点到当前结点的路径。 “剪枝”&#xff1a;当某一节点不包含问题的解&am…...

爬虫: 一文掌握 pycurl 的详细使用(更接近底层,性能更高)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、PycURL概述1.1 PycURL介绍1.2 基本安装1.3 安装依赖(Linux/macOS)1.4 常用选项参考二、基本使用2.1 简单 GET 请求2.2 获取响应信息2.3 设置请求头2.4 超时设置2.5 跟随重定向三、高级功能3.1 POST 请求3.2 文件上…...

大模型文生图

提示词分4个部分&#xff1a;质量&#xff0c;主体&#xff0c;元素&#xff0c;风格 质量&#xff1a;杰作&#xff0c;高质量&#xff0c;超细节&#xff0c;完美的精度&#xff0c;高分辨率&#xff0c;大师级的&#xff1b; 权重&#xff1a;把图片加括号&#xff0c;&am…...

c# AI编程助手 — Fitten Code

前言 前有Copilot各种酷炫操作&#xff0c;今有国产软件杀出重围。给大家介绍的是一款国内的国产编程神器&#xff0c;可与微软GitHub Copilot比比身手。关键它还是完全免费。它就是&#xff1a;非十团队国产自主研发的Fitten Code。此工具的速度是GitHub Copilot的两倍&#x…...

《植物大战僵尸融合版v2.4.1》,塔防与创新融合的完美碰撞

《植物大战僵尸融合版》是基于经典塔防游戏《植物大战僵尸》的创意同人改版&#xff0c;由“蓝飘飘fly”等开发者主导制作。它在保留原版核心玩法的基础上&#xff0c;引入了独特的植物融合机制&#xff0c;玩家可以将不同的植物进行组合&#xff0c;创造出全新的植物种类&…...

深度学习总结(12)

层:深度学习的基础模块 神经网络的基本数据结构是层。层是一个数据处理模块&#xff0c;它接收一个或多个张量作为输入&#xff0c;并输出一个或多个张量。有些层是无状态的&#xff0c;但大多数层具有状态&#xff0c;即层的权重。权重是利用随机梯度下降学到的一个或多个张量…...

pyqt环境配置

文章目录 1 概述2 PyQt6和PySide6区别3 环境配置4 配置PySide65 配置PyQt66 配置外部工具7 添加模板8 使用pyside6-project构建工程9 常见错误10 相关地址 更多精彩内容&#x1f449;内容导航 &#x1f448;&#x1f449;Qt开发 &#x1f448;&#x1f449;python开发 &#x1…...

YOLO11改进——融合BAM注意力机制增强图像分类与目标检测能力

深度学习在计算机视觉领域的应用取得了显著进展&#xff0c;尤其是在目标检测&#xff08;Object Detection&#xff09;和图像分类&#xff08;Image Classification&#xff09;任务中。YOLO&#xff08;You Only Look Once&#xff09;系列算法凭借其高效的单阶段检测框架和…...

考研单词笔记 2025.04.14

amount n数量&#xff0c;数额v&#xff08;数量&#xff09;达到&#xff0c;总计&#xff08;to&#xff09;&#xff0c;意味着&#xff0c;相当于 couple n一对&#xff0c;一双&#xff0c;一些&#xff0c;几个&#xff0c;夫妻&#xff0c;情侣v连接&#xff0c;结合 …...

AI云游戏盒子:未来娱乐的新纪元

AI云游戏盒子&#xff1a;未来娱乐的新纪元 随着科技的不断进步&#xff0c;人工智能&#xff08;AI&#xff09;与云计算技术的结合正在重新定义我们享受数字娱乐的方式。2025年&#xff0c;一款名为“AI云游戏盒子”的产品正逐渐成为家庭娱乐的核心设备&#xff0c;它不仅集…...

第八章 文件操作

第八章 文件操作 文章目录 第八章 文件操作1 文件读取1 将文件整个读取内存2 按字节读取文件 1 文件读取 1 将文件整个读取内存 类似于python的 with open(filename, modert, encodingutf-8) as f:res f.read()go中的书写方式&#xff1a; 方式一&#xff1a; package ma…...

《extern:如何在编译时“暗通款曲“》

C中extern关键字的完整用法总结 extern是C中管理链接性&#xff08;linkage&#xff09;的重要关键字&#xff0c;主要用于声明外部定义的变量或函数。以下是详细的用法分类和完整示例&#xff1a; 一、基本用法 1. 声明外部全局变量 // globals.cpp int g_globalVar 42; …...

活动图与流程图的区别与联系:深入理解两种建模工具

目录 前言1. 活动图概述1.1 活动图的定义1.2 活动图的基本构成要素1.3 活动图的应用场景 2. 流程图概述2.1 流程图的定义2.2 流程图的基本构成要素2.3 流程图的应用场景 3. 活动图与流程图的联系4. 活动图与流程图的区别4.1 所属体系不同4.2 表达能力差异4.3 使用目的与语境4.4…...

FinanceRAG获奖方案解读:ACM-ICAIF ’24的FinanceRAG挑战赛

ACM-ICAIF 24 FinanceRAG Challenge提供一套整合的文本和表格财务数据集。这些数据集旨在测试系统检索和推理财务数据的能力。参与者将受益于 Github 上的基线示例和官方提交代码&#xff0c;其位于FinanceRAG&#xff0c;以及在 huggingface 上的简化的数据集访问&#xff0c;…...

Linux LED驱动(gpio子系统)

0. gpio子系统 gpio子系统是linux内核当中用于管理GPIO资源的一套系统&#xff0c;它提供了很多GPIO相关的API接口&#xff0c;驱动程序中使用GPIO之前需要向gpio子系统申请。 gpio子系统的主要目的就是方便驱动开发者使用gpio&#xff0c;驱动开发者在设备树中添加gpio相关信息…...

场外期权交易和结算方式的区别是什么?

场外期权交易的核心在于双方协商一致的合约条款。这些条款包括但不限于期权的类型&#xff08;看涨或看跌&#xff09;、执行价格、到期日、合约规模以及支付的期权费。由于每份合约都是独一无二的&#xff0c;因此交易双方需要具备高度的专业知识和谈判技巧&#xff0c;下文为…...

自注意力的机制内涵和设计逻辑

在自注意力机制中&#xff0c;查询&#xff08;Q&#xff09;、键&#xff08;K&#xff09;和值&#xff08;V&#xff09;的交互过程是核心设计&#xff0c;其背后的数学和语义内涵可以从以下角度理解&#xff1a; 1. 数学视角&#xff1a;动态加权聚合 自注意力机制的公式可…...

VIM学习笔记

1. ex模式 vim中&#xff0c;按&#xff1a;触发的命令行模式&#xff0c;称为 ex模式&#xff0c;具体命令参见如下笔记&#xff1a; https://blog.csdn.net/u010250151/article/details/51868751?ops_request_misc%257B%2522request%255Fid%2522%253A%2522814b671a9898c95…...

Windows系统docker desktop安装(学习记录)

目前在学习docker&#xff0c;在网上扒了很多老师的教程&#xff0c;终于装好了&#xff0c;于是决定再装一遍做个记录&#xff0c;省的以后再这么麻烦 一&#xff1a;什么是docker Docker 是一个开源的应用容器引擎&#xff0c;它可以让开发者打包他们的应用以及依赖包到一个…...

操作系统学习笔记——[特殊字符]超详细 | 如何唤醒被阻塞的 socket 线程?线程阻塞原理、线程池、fork/vfork彻底讲明白!

&#x1f4a1;超详细 | 如何唤醒被阻塞的 socket 线程&#xff1f;线程阻塞原理、线程池、fork/vfork彻底讲明白&#xff01; 一、什么是阻塞&#xff1f;为什么线程会阻塞&#xff1f;二、socket线程被阻塞的典型场景&#x1f9e0; 解法思路&#xff1a; 三、线程的几种阻塞状…...

GIC驱动程序对中断的处理流程

承接上一篇&#xff0c;我们来讲讲GIC的处理流程&#xff1a; 我们先来看看老版本的CPU是怎么去处理中断的&#xff1a; 这种的话&#xff0c;基本上就是一开始就确定你有多少个中断&#xff0c;就为你分配好了多少个irq_desc&#xff0c;这样子每一个硬件中断&#xff0c;也就…...

罗庚机器人:机器人打磨领域的先行者

近日&#xff0c;记者在广东罗庚机器人有限公司&#xff08;以下简称罗庚机器人&#xff09;总经理蒲小平处了解到&#xff0c;该公司是一家研发与为客户提供高精度自适应机器人打磨抛光集成工艺的高科技企业&#xff0c;是机器人打磨领域的先行者。在国内外机器人打磨抛光应用…...

如何在 Java 中对 PDF 文件进行数字签名(教程)

Java 本身并不原生支持 PDF 文件&#xff0c;因此若要对 PDF 进行数字签名&#xff0c;您需要使用一些专用的软件。本教程将演示如何使用 JPedal PDF 库来对 PDF 文件进行数字签名。 步骤&#xff1a; • 下载 JPedal 并将 Jar 文件添加到项目中 • 创建一个 PKCS#12 密…...

一文了解:北斗短报文终端是什么,有哪些应用场景?

在通信技术飞速发展的今天&#xff0c;人们已习惯于依赖地面基站和互联网实现即时通信。然而&#xff0c;当自然灾害突发、远洋航行遇险或深入无人区勘探时&#xff0c;传统通信手段往往失效。北斗短报文终端——这一由中国自主研发的卫星通信技术&#xff0c;正以“无网络通信…...

【KWDB创作者计划】_KWDB应用之实战案例

【KWDB 2025 创作者计划】_KWDB应用之实战案例 本文是在完成KWDB数据库安装的情况下的操作篇&#xff0c;关于KWDB的介绍与安装部署&#xff0c;可以查看上一篇博客: https://blog.itpub.net/70045384/viewspace-3081187/ https://blog.csdn.net/m0_38139250/article/details/…...

JavaScript中的运算符与语句:深入理解编程的基础构建块

# JavaScript中的运算符与语句&#xff1a;深入理解编程的基础构建块 在JavaScript编程的世界里&#xff0c;运算符和语句就像是构建大厦的基石&#xff0c;它们是编写高效、灵活代码的基础。今天&#xff0c;我们就来深入了解一下这些重要的元素。 ## 一、运算符&#xff1a;…...

MySQL——学习InnoDB(1)

MySQL——学习InnoDB&#xff08;1&#xff09; 文章目录 MySQL——学习InnoDB&#xff08;1&#xff09;1. InnoDB的前世今生1.1 诞生发展1.2 核心设计1.3 关键进化 2. InnoDB和MyISAM在性能和安全上的区别2.1 性能对比2.2 安全对比 3. InnoDB架构图&#xff08;内存结构磁盘结…...

QT中多线程写法

转自个人博客&#xff1a;QT中多线程写法 1. QThread及moveToThread() 使用情况&#xff1a; 多使用于需要将有着复杂逻辑或需要一直占用并运行的类放入子线程中执行的情况&#xff0c;moveToThread是将整个类的对象移入子线程。 优缺点&#xff1a; 优点&#xff1a;更符合…...

css 二维码始终显示在按钮的正下方,并且根据不同的屏幕分辨率自动调整位置

一、需求 “求职入口” 下面的浮窗位置在其正下方&#xff0c;并且浏览器分辨的改变&#xff08;拖动浏览器&#xff09;&#xff0c;位置依旧在最下方 二、实现 <div class"btn_box"><div class"btn_link id"js-apply">求职入口<di…...

STM32 认识STM32

目录 什么是嵌入式&#xff1f; 认识STM32单片机 开发环境安装 安装开发环境 开发板资源介绍 单片机开发模式 创建工程的方式 烧录STM32程序 什么是嵌入式&#xff1f; 1.智能手环项目 主要功能有&#xff1a; 彩色触摸屏 显示时间 健康信息&#xff1a;心率&#…...

关于 CSDN的C知道功能模块 的详细解析,包括 新增的AI搜索(可选深度思考) 和 智能体功能 的具体说明及对比分析

以下是关于 CSDN的C知道功能模块 的详细解析&#xff0c;包括 新增的AI搜索&#xff08;可选深度思考&#xff09; 和 智能体功能 的具体说明及对比分析&#xff1a; 一、C知道核心功能模块详解&#xff08;基础功能&#xff09; &#xff08;参考前文内容&#xff0c;此处略…...

Vue--组件练习案例

图片轮播案例&#xff1a; <!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><title>Title</title></head><body><!--轮播图片--><div id"app"><h1>轮播图</h1…...

Sentinel源码—1.使用演示和简介一

大纲 1.Sentinel流量治理框架简介 2.Sentinel源码编译及Demo演示 3.Dashboard功能介绍 4.流控规则使用演示 5.熔断规则使用演示 6.热点规则使用演示 7.授权规则使用演示 8.系统规则使用演示 9.集群流控使用演示 1.Sentinel流量治理框架简介 (1)与Sentinel相关的问题 …...

空间信息可视化——WebGIS前端实例(二)

技术栈&#xff1a;原生HTML 源代码&#xff1a;CUGLin/WebGIS: This is a project of Spatial information visualization 5 水质情况实时监测预警系统 5.1 系统设计思想 水安全是涉及国家长治久安的大事。多年来&#xff0c;为相应国家战略&#xff0c;诸多地理信息领域的…...

高并发内存池(前言介绍)

高并发内存池项目介绍 1.项目介绍2.项目的知识储备要求3.了解池化技术及内存池4.本次项目中内存池解决的问题5.malloc 1.项目介绍 此项目是实现一个高并发的内存池&#xff0c;它的原型是google的一个开源项目tcmalloc&#xff0c;tcmalloc全程Thread-Caching Malloc&#xff…...

误译、值对象和DDD伪创新-《分析模式》漫谈56

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 “Analysis Patterns”的第14章“类型模型设计模板模式”原文&#xff1a; Examples of such object types are the classic built-in data types of programming environments: inte…...

Java单例模式:实现全局唯一对象的艺术

精心整理了最新的面试资料和简历模板&#xff0c;有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 一、什么是单例模式&#xff1f; 单例模式&#xff08;Singleton Pattern&#xff09;是一种创建型设计模式&#xff0c;确保一个类只有一个实例&#xff0c…...

机器学习项目二:帕金森病检测

目录 下载数据 一、导入相关包 二、数据加载 三、特征工程 四、构建模型 五、评估与可视化 六、程序流程 七、完整代码 一、导入相关包 # 导入库部分 import numpy as np # 数值计算基础库 import pandas as pd # 数据处理库 from sklearn.preprocessing import MinMaxS…...

FreeDogs:AI、区块链与迷因文化的深度融合

引言 在 Web3 时代&#xff0c;人工智能&#xff08;AI&#xff09;、区块链技术和迷因文化的结合催生了一种全新的去中心化生态系统。FreeDogs 项目作为这一领域的创新代表&#xff0c;通过独特的技术与文化融合模式迅速受到关注。它利用 AI 驱动的智能营销网络推动迷因文化的…...

《组合优于继承:构建高内聚低耦合模块的最佳实践》

《组合优于继承:构建高内聚低耦合模块的最佳实践》 一、引言:编程范式中的选择 在软件设计中,继承和组合是两个重要的设计模式。继承(Inheritance)常被用来实现代码复用,但滥用继承容易导致脆弱的类层次结构,增加耦合度和维护成本。而组合(Composition)通过将功能分解…...