概述
使用接口能够让我们写出易于测试的代码,然而很多工程师对 Go 的接口了解都非常有限,也不清楚其底层的实现原理,这成为了开发可维护、可测试优秀代码的阻碍。
本节会介绍使用接口时遇到的一些常见问题以及它的设计与实现,包括接口的类型转换、类型断言以及动态派发机制,帮助各位读者更好地理解接口类型。
在计算机科学中,接口是计算机系统中多个组件共享的边界,不同的组件能够在边界上交换信息。如下图所示,接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
图1 上下游通过接口解耦
这种面向接口的编程方式有着非常强大的生命力,无论是在框架还是操作系统中我们都能够找到接口的身影。可移植操作系统接口(Portable Operating System Interface,POSIX)2就是一个典型的例子,它定义了应用程序接口和命令行等标准,为计算机软件带来了可移植性 — 只要操作系统实现了 POSIX,计算机软件就可以直接在不同操作系统上运行。
除了解耦有依赖关系的上下游,接口还能够帮助我们隐藏底层实现,减少关注点。《计算机程序的构造和解释》中有这么一句话:
代码必须能够被人阅读,只是机器恰好可以执行3
人能够同时处理的信息非常有限,定义良好的接口能够隔离底层的实现,让我们将重点放在当前的代码片段中。
什么是接口?
定义
官方文档 中对 Interface 是这样定义的:
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil. Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here. (https://go.dev/doc/effective_go#interfaces_and_types)
一个 interface 类型定义了一个 “函数集” 作为其接口。 interface 类型的变量可以保存含有属于这个 interface 类型方法集超集的任何类型的值,这时我们就说这个类型 实现 了这个 接口。未被初始化的 interface 类型变量的零值为 nil。
对于 interface 类型的方法集来说,其中每一个方法都必须有一个不重复并且不是 补位名(即单下划线 _)的方法名。
动态派发(Dynamic dispatch)
Go 接口又称为动态数据类型(抽象类型),在使用接口的的时候, 会动态指向具体类型(结构体)。
动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
类型系统的核心
Go语言的主要设计者之一罗布·派克曾经说过:
如果只能选择一个Go语言的特性移植到其他语言中,我会选择接口。(Rob Pike)
接口在Go语言有着至关重要的地位。如果说goroutine和channel 是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代成为一道极为亮丽的风景,那么接口是Go语言整个类型系统的基石,让Go语言在基础编程哲学的探索上达到前所未有的高度。
Go语言中Interface淡化了面向对象中接口应具有的象征意义,接口在Go语言中仅仅只是“表现形式”上相同的一类事物的抽象概念。在Go语言中只要是具有相同“表现形式”的“类型”都具有相同的Interface,而不需要考虑这个Interface在具体的使用中应具有的实际意义。
interface 特性小结
- 是一组函数签名的集合
- 是一种类型
面向接口编程思想
- 模块之间依赖接口以实现继承和多态特性。
- 继承和多态是面向对象设计一个非常好的特性,它可以更好的抽象框架,让模块之间依赖于接口,而不是依赖于具体实现。
- 依赖于接口来实现方法函数,只要实现了这个接口就可以认为赋值给这个口,实现动态绑定。
如何定义一个接口?
代码语言:javascript复制type IInsightMultiMarketOverviewService interface {
GetMultiMarketSummaryPriceBandDistributionDataTable(ctx context.Context, multiMarketId int64, selfDefineId int64) ([]map[string]interface{}, error)
GetMultiMarketSummaryPriceBandDistributionQuadrant(ctx context.Context, multiMarketId int64) (*indexu.IQuadrantListType, error)
service_insight_multi_market.IInsightMultiMarketService
rocket.IRocketFetcher
}
type IInsightMultiMarketService interface {
// GetMultiIdTimeRange 获取多市场ID的 分析时间范围 和 对比时间范围
GetMultiIdTimeRange(ctx context.Context, multiId int64) (analysisRange, comparisonRange *common.TimeRange, err error)
// GetMultiMarketAnalysisMap 获取多市场ID对应的细分市场列表
GetMultiMarketAnalysisMap(ctx context.Context, multiId int64) (analysisMarketMap map[int64]*model.BrandCustomerMarket, err error)
// GetMultiMarketComparisonId 根据组合 ID 获取下面所有的 (分析市场 ID,对比市场 ID) 元组信息
GetMultiMarketAnalysisComparisonIds(ctx context.Context, multiId int64) (analysisComparisonIdRef []*model.BrandCustomerMultiMarketRef, err error)
}
type IRocketFetcher interface {
service.BasicInfoService
driver.INavigatorDriver
}
type RocketFetcher struct {
service.BasicInfoService
driver.INavigatorDriver
}
func NewRocketFetcher() *RocketFetcher {
return &RocketFetcher{
&service.BasicInfoServiceImpl{},
&driver.NavigatorDriver{},
}
}
如何实现接口?
定义接口:
代码语言:javascript复制type INavigatorDriver interface {
Query(ctx context.Context,
sqlKey,
sql string,
SearchOptions []*engine.Option,
SqlClient *sqlclient.SQLClient,
) ([]map[string]interface{}, error)
}
type NavigatorDriver struct {
}
func NewNavigatorDriver() *NavigatorDriver {
return &NavigatorDriver{}
}
实现接口:
代码语言:javascript复制// Query by sql
func (rcvr *NavigatorDriver) Query(Ctx context.Context,
sqlKey,
sql string,
SearchOptions []*engine.Option,
SqlClient *sqlclient.SQLClient,
) ([]map[string]interface{}, error) {
logu.CtxInfo(Ctx, "Navigator Query", "sqlKey: %v, sql:%v", sqlKey, sql)
return NavigatorQueryList(Ctx, sqlKey, sql, SqlClient, SearchOptions...)
}
性能注意点
使用结构体实现接口带来的开销会大于使用指针实现,而动态派发在结构体上的表现非常差。使用结构体带来的巨大性能差异不只是接口带来的问题,带来性能问题,主要因为 Go 语言在函数调用时是传值的,动态派发的过程只是放大了参数拷贝带来的影响。
类型断言
根据变量不同的类型进行不同的操作。
① 类型断言方法一
代码语言:javascript复制func judgeType1(q interface{}) {
temp, ok := q.(string)
if ok {
fmt.Println("类型转换成功!", temp)
} else {
fmt.Println("类型转换失败!", temp)
}
}
① 类型断言方法二
使用switch...case...语句,如果断言成功则到指定分支。
代码如下(示例):
code1:普通类型
代码语言:javascript复制func judgeType2(q interface{}) {
switch i := q.(type) {
case string:
fmt.Println("这是一个字符串!", i)
case int:
fmt.Println("这是一个整数!", i)
case bool:
fmt.Println("这是一个布尔类型!", i)
default:
fmt.Println("未知类型", i)
}
}
code2:指针类型
代码语言:javascript复制func main() {
var c Duck = &Cat{Name: "draven"}
switch c.(type) {
case *Cat:
cat := c.(*Cat)
cat.Quack()
}
}
接口的嵌套
接口可以进行嵌套实现,通过大接口包含小接口。
代码语言:javascript复制type IInsightMultiMarketOverviewService interface {
GetMultiMarketSummaryPriceBandDistributionDataTable(ctx context.Context, multiMarketId int64, selfDefineId int64) ([]map[string]interface{}, error)
GetMultiMarketSummaryPriceBandDistributionQuadrant(ctx context.Context, multiMarketId int64) (*indexu.IQuadrantListType, error)
service_insight_multi_market.IInsightMultiMarketService
rocket.IRocketFetcher
}
type IRocketFetcher interface {
service.BasicInfoService
driver.INavigatorDriver
}
type RocketFetcher struct {
service.BasicInfoService
driver.INavigatorDriver
}
func NewRocketFetcher() *RocketFetcher {
return &RocketFetcher{
&service.BasicInfoServiceImpl{},
&driver.NavigatorDriver{},
}
}
gomock 接口测试
- 安装mockgen环境,生成 mock 测试桩代码
- Go Mock 接口测试 单元测试 极简教程:https://cloud.tencent.com/developer/article/2012966
- Go 接口嵌套组合的使用方法 & gomock 测试 stub 代码生成:https://cloud.tencent.com/developer/article/2016044
- gomock mockgen : unknown embedded interface: https://cloud.tencent.com/developer/article/2014663
mockgen_service_insight_multi_market:
mockgen -source=./service/service_insight_multi_market/service_insight_multi_market.go -destination ./service/service_insight_multi_market/service_insight_multi_market_mock.go -package service_insight_multi_market
mockgen_service_insight_multi_market_overview:
mockgen -source=./service/service_insight_multi_market_overview/service_insight_multi_market_overview.go -destination ./service/service_insight_multi_market_overview/service_insight_multi_market_overview_mock.go -package service_insight_multi_market_overview -aux_files service_insight_multi_market_overview=./service/service_insight_multi_market/service_insight_multi_market.go
- mock 测试代码实例
func Test_InsightMultiMarketHandler_GetMultiMarketSummaryPriceBandDistributionDataTable(t *testing.T) {
ctx := context.Background()
multiMarketId := int64(123)
selfDefineId := int64(1)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
MockIInsightMultiMarketService := service_insight_multi_market.NewMockIInsightMultiMarketService(ctrl)
// 调用 InsightMultiMarketService.GetMultiIdTimeRange
MockIInsightMultiMarketService.
EXPECT().
GetMultiIdTimeRange(gomock.Any(), gomock.Any()).
Return(&common.TimeRange{StartDate: 1654701220}, &common.TimeRange{StartDate: 1653177600}, nil)
// 调用 InsightMultiMarketService.GetMultiMarketAnalysisComparisonIds
MockIInsightMultiMarketService.
EXPECT().
GetMultiMarketAnalysisComparisonIds(gomock.Any(), gomock.Any()).
Return([]*model.BrandCustomerMultiMarketRef{
{MultiMarketID: 123, MarketID: 1, ComparisonID: 4},
{MultiMarketID: 123, MarketID: 2, ComparisonID: 5},
{MultiMarketID: 123, MarketID: 3, ComparisonID: 6},
}, nil)
// UIComponent 唯一 Render() 数据函数
mockRocketFetcher := rocket.NewMockIRocketFetcher(ctrl)
mockRocketFetcher.
EXPECT().
Query(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(driver.Mock_app_compass_strategy_multi_market_property_hi1())
mockRocketFetcher.
EXPECT().
Query(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(driver.Mock_app_compass_strategy_multi_market_property_hi2())
s := &service_insight_multi_market_overview.InsightMultiMarketOverviewService{
MockIInsightMultiMarketService,
mockRocketFetcher,
}
result, _ := s.GetMultiMarketSummaryPriceBandDistributionDataTable(ctx, multiMarketId, selfDefineId)
fmt.Println("result=", convert.ToJSONString(result))
IInsightMultiMarketOverviewService := service_insight_multi_market_overview.NewMockIInsightMultiMarketOverviewService(ctrl)
IInsightMultiMarketOverviewService.
EXPECT().
GetMultiMarketSummaryPriceBandDistributionDataTable(gomock.Any(), gomock.Any(), gomock.Any()).
Return(result, nil)
InsightMultiMarketHandler := &InsightMultiMarketHandler{
service_insight_multi_market.NewInsightMultiMarketServiceHandler(),
IInsightMultiMarketOverviewService,
}
req := &multi_market_overview.MultiMarketSummaryPriceBandDistributionDataTableReq{
MultiMarketId: "123",
SelfDefineId: "1",
}
resp, _ := InsightMultiMarketHandler.GetMultiMarketSummaryPriceBandDistributionDataTable(ctx, req)
resultJSONString := convert.ToJSONString(resp)
fmt.Println("resp=", resultJSONString)
wanted := "{"data":{"datatable":[{"dimention":"pay_amt","dimention_name":"销售金额","price_brand":"-999","index_info":{"value":7924,"out_period_incr":-0.23476581361661034,..."
if resultJSONString != wanted {
t.Errorf("Test TestGetMultiMarketSummaryPriceBandDistributionDataTable failed, wanted %v, got %v", wanted, resultJSONString)
}
}
接口实现原理篇【高阶篇】
参考: Go 接口实现原理【高阶篇】: type _interface struct :
https://cloud.tencent.com/developer/article/2020962
总结
接口使用较为灵活,可以在实现的接口内进行本类型对象的操作,在接口外部进行接口方法调用,实现相同的代码段有不同的效果,多态的思想也尤为重要,灵活使用接口,使程序更加灵活是每一名程序员的愿望。
参考资料
https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/
https://www.tapirgames.com/blog/golang-interface-implementation
https://go.dev/doc/effective_go#interfaces_and_types
https://blog.csdn.net/apple_51931783/article/details/122458612
https://blog.csdn.net/qq_21794823/article/details/78967719
https://blog.csdn.net/jacob_007/article/details/53557074
https://stackoverflow.com/questions/55999405/how-can-i-mock-specific-embedded-method-inside-interface
https://pkg.go.dev/github.com/golang/mock/gomock
https://github.com/golang/mock#running-mockgen