美文网首页
Go 面向接口编程实战

Go 面向接口编程实战

作者: 光剑书架上的书 | 来源:发表于2022-06-11 14:08 被阅读0次

    概述

    使用接口能够让我们写出易于测试的代码,然而很多工程师对 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. 依赖于接口来实现方法函数,只要实现了这个接口就可以认为赋值给这个口,实现动态绑定。

    如何定义一个接口?

    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{},
        }
    }
    
    

    如何实现接口?

    定义接口:

    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{}
    }
    

    实现接口:

    // 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 语言在函数调用时是传值的,动态派发的过程只是放大了参数拷贝带来的影响。

    类型断言

    根据变量不同的类型进行不同的操作。
    ① 类型断言方法一

    func judgeType1(q interface{}) {
        temp, ok := q.(string)
        if ok {
            fmt.Println("类型转换成功!", temp)
        } else {
            fmt.Println("类型转换失败!", temp)
        }
    
    }
    

    ① 类型断言方法二

    使用switch...case...语句,如果断言成功则到指定分支。

    代码如下(示例):

    code1:普通类型

    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:指针类型

    func main() {
        var c Duck = &Cat{Name: "draven"}
        switch c.(type) {
        case *Cat:
            cat := c.(*Cat)
            cat.Quack()
        }
    }
    

    接口的嵌套

    接口可以进行嵌套实现,通过大接口包含小接口。

    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 测试桩代码
    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 测试代码实例
    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://www.jianshu.com/p/93082b312512

    总结

    接口使用较为灵活,可以在实现的接口内进行本类型对象的操作,在接口外部进行接口方法调用,实现相同的代码段有不同的效果,多态的思想也尤为重要,灵活使用接口,使程序更加灵活是每一名程序员的愿望。

    参考资料

    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

    相关文章

      网友评论

          本文标题:Go 面向接口编程实战

          本文链接:https://www.haomeiwen.com/subject/cloimrtx.html