Go 面向接口编程实战

Posted 禅与计算机程序设计艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 面向接口编程实战相关的知识,希望对你有一定的参考价值。

概述

使用接口能够让我们写出易于测试的代码,然而很多工程师对 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.IRocketFetchertype 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.INavigatorDrivertype RocketFetcher struct 
    service.BasicInfoService
    driver.INavigatorDriverfunc 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 sqlfunc (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 = &CatName: "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.IRocketFetchertype IRocketFetcher interface 
    service.BasicInfoService
    driver.INavigatorDrivertype RocketFetcher struct 
    service.BasicInfoService
    driver.INavigatorDriverfunc NewRocketFetcher() *RocketFetcher 
    return &RocketFetcher
        &service.BasicInfoServiceImpl,
        &driver.NavigatorDriver,
    

gomock 接口测试

  1. 安装mockgen环境,生成 mock 测试桩代码

  • Go Mock 接口测试 单元测试 极简教程:https://www.jianshu.com/p/abcb14f4bdf1

  • Go 接口嵌套组合的使用方法 & gomock 测试 stub 代码生成:https://www.jianshu.com/p/f1f09aa28ca9

  • gomock mockgen : unknown embedded interface: https://www.jianshu.com/p/a1233aa9347f

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.TimeRangeStartDate: 1654701220, &common.TimeRangeStartDate: 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 面向接口编程实战的主要内容,如果未能解决你的问题,请参考以下文章

Go Web编程实战----面向对象编程

Go Web编程实战----面向对象编程

Go Web编程实战----面向对象编程

Go Web编程实战----面向对象编程

谷雨课堂Go实战 No.010 Go干货!面向对象与函数式编程

Go基础函数和面向接口编程