前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 面向接口编程实战

Go 面向接口编程实战

作者头像
一个会写诗的程序员
发布2022-06-12 16:50:47
1.9K0
发布2022-06-12 16:50:47
举报

概述

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 什么是接口?
    • 定义
      • 动态派发(Dynamic dispatch)
        • 类型系统的核心
          • interface 特性小结
          • 面向接口编程思想
          • 如何定义一个接口?
          • 如何实现接口?
            • 性能注意点
            • 类型断言
            • 接口的嵌套
            • gomock 接口测试
            • 接口实现原理篇【高阶篇】
            • 总结
            • 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档