Go 面向接口编程实战

2022-06-12 16:50:47 浏览数 (1)

概述

使用接口能够让我们写出易于测试的代码,然而很多工程师对 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 特性小结

  • 是一组函数签名的集合
  • 是一种类型

面向接口编程思想

  1. 模块之间依赖接口以实现继承和多态特性。
  2. 继承和多态是面向对象设计一个非常好的特性,它可以更好的抽象框架,让模块之间依赖于接口,而不是依赖于具体实现。
  3. 依赖于接口来实现方法函数,只要实现了这个接口就可以认为赋值给这个口,实现动态绑定。

如何定义一个接口?

代码语言: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 接口测试

  1. 安装mockgen环境,生成 mock 测试桩代码
  2. Go Mock 接口测试 单元测试 极简教程:https://cloud.tencent.com/developer/article/2012966
  3. Go 接口嵌套组合的使用方法 & gomock 测试 stub 代码生成:https://cloud.tencent.com/developer/article/2016044
  4. gomock mockgen : unknown embedded interface: https://cloud.tencent.com/developer/article/2014663
代码语言:javascript复制
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
  1. mock 测试代码实例
代码语言:javascript复制
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

0 人点赞