基础概念
鸭子模型
那什么鸭子模型?
鸭子模型的解释,通常会用了一个非常有趣的例子,一个东西究竟是不是鸭子,取决于它的能力。游泳起来像鸭子、叫起来也像鸭子,那么就可以是鸭子。
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 的方式,赋予方法更高的灵活度。
将不同的功能分开,暴露给不同的接口,隐藏不必要的方法,增强代码的封装性。
外部代码只需要依赖接口,而不需要关系具体实现类的详细方法。
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/