基础概念

鸭子模型

那什么鸭子模型?

鸭子模型的解释,通常会用了一个非常有趣的例子,一个东西究竟是不是鸭子,取决于它的能力。游泳起来像鸭子、叫起来也像鸭子,那么就可以是鸭子。

Go接口设计和鸭子模型有密切关系,但又和动态语言的鸭子模型有所区别,在编译时,即可实现必要的类型检查。

类型结构体

runtime._type 是Go语言类型的运行时表示,

type _type struct {
	size       uintptr
	ptrdata    uintptr
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}
  • size 字段存储了类型占用的内存空间,为内存空间的分配提供信息;

  • hash 字段能够帮助我们快速确定类型是否相等;

  • equal 字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的;

eface结构体

不携带任何方法的 interface{} 类型底层实现为 eface 结构体

// src/runtime/runtime2.go
type eface struct {
	_type *_type
	data  unsafe.Pointer
}

eface结构体主要包含两个字段,总共16字节

  • type:运行时【runtime.type】指针类型

  • data:数据指针

// src/runtime/type.go

// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
// ../internal/reflectlite/type.go:/^type.rtype.
type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

iface结构体

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

iface同eface一样,也是共占用16字节

tab字段不仅被用来存储接口本身的信息(例如接口的类型信息、方法集信息等),还被用来存储具体类型所实现的信息。tab字段是一个itab struct的指针,每个itab占用32字节。

data字段"指向"当前被赋值给接口类型变量的具体类型变量的值。

// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptabs.
type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
  • hash:字段拷贝自_type.hash,用于类型切换,当我们想将interface类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致

  • fun:固定大小的数组,存储第一个方法的函数指针,如果fun[0]为''是,说明_type并没有实现该接口

  • inter是一个interfaccetype类型,基于_type字段的基础上,增加一些额外的字段来进行管理的

接口的作用

1. 多态

接口支持多态,允许不同类型实现相同的接口,并通过接口遍历调用方法。

package main

import "fmt"

// 定义接口
type Speaker interface {
    Speak() string
}

// 结构体实现接口
type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}

func (c Cat) Speak() string {
    return "Meow!"
}

func MakeSound(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{}
    cat := Cat{}

    MakeSound(dog) // 输出: Woof!
    MakeSound(cat) // 输出: Meow!
}

2. 空接口

空接口可以表示任何类型,常用于泛型数据的处理。

func PrintValue(v interface{}) {
    fmt.Println(v)
}

func main() {
    PrintValue(42)       // 输出: 42
    PrintValue("hello")  // 输出: hello
    PrintValue(true)     // 输出: true
}

3. 接口类型断言

使用类型断言可以将interface{}还原为具体类型。

func Describe(i interface{}) {
    if v, ok := i.(string); ok {
        fmt.Println("String:", v)
    } else if v, ok := i.(int); ok {
        fmt.Println("Integer:", v)
    } else {
        fmt.Println("Unknown type")
    }
}

func main() {
    Describe("hello")  // 输出: String: hello
    Describe(100)      // 输出: Integer: 100
    Describe(true)     // 输出: Unknown type
}

4. 接口类型选择(switch语句)

可以使用switch语句来处理不同的接口类型。

func DetectType(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Println("Integer:", v)
    case string:
        fmt.Println("String:", v)
    case bool:
        fmt.Println("Boolean:", v)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    DetectType(10)      // 输出: Integer: 10
    DetectType("Hi")    // 输出: String: Hi
    DetectType(false)   // 输出: Boolean: false
}

5. 自定义错误接口

Golang 的 error 类型就是一个接口,开发者可以实现自定义错误类型。

type MyError struct {
    Msg string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("Error: %s", e.Msg)
}

func DoSomething(fail bool) error {
    if fail {
        return &MyError{"Something went wrong"}
    }
    return nil
}

func main() {
    err := DoSomething(true)
    if err != nil {
        fmt.Println(err)
    }
}

6. 通过接口隐藏细节

在模块间进行类的传输时,为了保护具体的实现类隐藏其中的实现细节,转而使用抽象 interface 的形式进行传递. 同时这种基于 interface 进行传递参数的方式,也基于了使用方一定的灵活度,可以通过注入 interface 不同实现 class 的方式,赋予方法更高的灵活度。

  1. 将不同的功能分开,暴露给不同的接口,隐藏不必要的方法,增强代码的封装性。

  2. 外部代码只需要依赖接口,而不需要关系具体实现类的详细方法。

package main

import "fmt"

// 定义一个可以管理员工的接口,暴露员工的基本信息方法
type EmployeeInfo interface {
	GetName() string
	GetAge() int
}

// 定义一个可以管理员工工资的接口,暴露工资管理方法
type SalaryManager interface {
	CalculateSalary() float64
	PaySalary() string
}

// 实现 EmployeeInfo 和 SalaryManager 接口的 Employee 结构体
type Employee struct {
	Name     string
	Age      int
	Salary   float64
	Position string
}

func (e *Employee) GetName() string {
	return e.Name
}

func (e *Employee) GetAge() int {
	return e.Age
}

func (e *Employee) CalculateSalary() float64 {
	// 假设简单计算工资:职位与基础工资
	if e.Position == "Manager" {
		return e.Salary * 1.5
	}
	return e.Salary
}

func (e *Employee) PaySalary() string {
	return fmt.Sprintf("Paying salary: %.2f", e.CalculateSalary())
}

// 函数:根据需要提供不同的接口实例
func GetEmployeeInfo(e *Employee) EmployeeInfo {
	return e
}

func GetSalaryManager(e *Employee) SalaryManager {
	return e
}

func main() {
	// 创建 Employee 实例
	emp := &Employee{Name: "John", Age: 30, Salary: 5000, Position: "Manager"}

	// 获取员工基本信息
	info := GetEmployeeInfo(emp)
	fmt.Println("Employee Info:")
	fmt.Println("Name:", info.GetName())
	fmt.Println("Age:", info.GetAge())

	// 获取工资管理功能
	manager := GetSalaryManager(emp)
	fmt.Println("Salary Information:")
	fmt.Println(manager.PaySalary())
}

接口类型函数

在net/http包中,HandlerFunc为函数,实现了Handler接口。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

这样实现后,只要你的函数签名为:

func (ResponseWriter, *Request)

拥有http.HandlerFunc()一致的函数签名,就可以将该函数进行类型转换,转换为http.HandlerFunc函数类型。且http.HandlerFunc函数类型实现了Handler接口,则你命名的函数转换后也相当于实现了Handler接口。

我们可以 http.Handle 来映射请求路径和处理函数,Handle 的定义如下:

func Handle(pattern string, handler Handler)

第二个参数是即接口类型 Handler,我们可以这么用。

func home(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("hello, index page"))
}

func main() {
	http.Handle("/home", http.HandlerFunc(home))
	_ = http.ListenAndServe("localhost:8000", nil)
}

通常,我们还会使用另外一个函数 http.HandleFunc,HandleFunc 的定义如下:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

第二个参数是一个普通的函数类型,那可以直接将 home 传递给 HandleFunc:

func main() {
	http.HandleFunc("/home", home)
	_ = http.ListenAndServe("localhost:8000", nil)
}

那如果我们看过 HandleFunc 的内部实现的话,就会知道两种写法是完全等价的,内部将第二种写法转换为了第一种写法。

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

文章参考

https://zhuanlan.zhihu.com/p/639882670

https://www.zhihu.com/question/318138275/answer/699989214

https://blog.haohtml.com/archives/23047/

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/