您的位置 首页 golang

Go Interface 的优雅使用,让代码更整洁更容易测试

大家好,我是你们的章鱼猫。

在 Go 语言中,如果你还不会使用 Interface,那么你还没有真正掌握 Go 语言,Interface 是 Go 语言的精华和灵魂所在。接下来我们从一下几个方面介绍 Go 语言里面的 Interface。

Go Interface 是什么?

简单来说,Interface 是一组方法(Method)的集合,也是一种类型。比如如下是一个简单的 Interface 定义。

 type UserDataStore interface {
      GetUserScore(ctx context.Context, id string) (int, error)
      DeleteUser(ctx context.Context, id string) error
}  

另外,在 Go 里面是允许没有任何方法的 Interface,对于这样的空 Interface,可以认为任何的类型都实现了空 Interface。

假设,一个类型 A 实现了上述 Interface(UserDataStore)的方法,我们就可以认为 A 实现了上述 Interface,在实际的函数调用传参中 A 是可以直接作为 UserDataStore 类型的参数。是的,可以理解为这就是我们常说的多态。

Go Interface 能做什么?

那么 Interface 除了可以实现多态,实际可以用来做什么呢。以下是我认为比较重要的亮点。

  1. 依赖反转,让代码结构更整洁

我们来说一个比较常见的场景,一个 HTTP 接口,需要依赖数据库来获取用户得分并返回给调用方。比较直接的写法如下。

 db := orm.NewDatabaseConnection()  // 建立数据库链接
res := db.Query("select score from user where user == 'xxx'")   // 通过 SQL 语句查询数据
return HTTP.Json(res) // 通过 Json 返回给调用方  

这样写的坏处是,让 HTTP 的接口依赖了具体数据库底层的接口及实现,在数据库查询的功能没有开发完成时,HTTP 接口是不能开始开发的。同时对于如果后续存在更换数据库的可能,也不是很容易的扩展。比较推荐的写法是下面这样。

 type UserDataStore interface {      GetUserScore(ctx context.Context, id string) (int, error)
      DeleteUser(ctx context.Context, id string) error
}
// GetUserScoreHandler creates an HTTP handler that can get a user's score
func GetUserScoreHandler(userDataStore UserDataStore) http.HandlerFunc {
      return func(res http.ResponseWriter, req *http.Request) {
            id := req.Header.Get("x-user-id")
        score, err := userDataStore.GetUserScore(req.Context(), id)
            if err != nil {
                  fmt.Println("userDataStore.GetUserScore: ", err)
                  res.WriteHeader(500)
                  return
            }
            res.Write([]byte(fmt.Sprintf("%d", score)))
    }
}  

通过定义 Interface,将数据库与 HTTP 接口进行解耦,HTTP 接口不再依赖实际的数据库,代码可以单独的编写和编译,代码依赖和结构更加的清晰了。数据具体的实现逻辑只需按 Interface 实现对应的接口就可以了,最终实现了依赖的整体的反转。

  1. 提高程序的可测试性

回到刚才那个例子,如果我要对这个 HTTP 接口的逻辑做测试,我可以怎么做?如果你没有使用 Interface,那么测试肯定要依赖一个实际的 DB,我想你会去新建一个测试库,同时新建一些测试数据。

真的需要这样么?我们来一个比较好的实践。通过 Interface,可以很容易的实现一个 Mock 版本的类型,通过替换逻辑可以很方便的实现测试数据的构造。

 type mockUserDataStore struct {
      pendingError error
      pendingScore int
      deletedUsers []string
}
func (m *mockUserDataStore) GetUserScore(ctx context.Context, id string) (int, error) {
      return m.pendingScore, m.pendingError
}
func (m *mockUserDataStore) DeleteUser(ctx context.Context, id string) error {
      if m.pendingError != nil {
            return m.pendingError
    }
      m.deletedUsers = append(m.deletedUsers, id)
      return nil
}  

以上就可以很方便的去控制接口调用的时候,获取用户得分和删除用户的逻辑。实际的测试也就变得简单了,也不用依赖真实的 DB,让测试更加的可靠了。

 func TestGetUserScoreHandlerReturnsScore(t *testing.T) {
      req := httptest.NewRequest("GET", "/idk", nil)
      res := httptest.NewRecorder()
      userDataStore := &mockUserDataStore{
            pendingScore: 3,  // mock 数据
    }
      handler := GetUserScoreHandler(userDataStore)   // 将 Mock 的方法传递到实际调用的地方,实现动态的替换
      handler(res, req)
      resultStr := string(res.Body.Bytes())
      expected := fmt.Sprintf("%d", userDataStore.pendingScore)
      if res.Code != 200 {
            t.Errorf("Expected HTTP response 200 but got %d", res.Code)
    }
      if resultStr != expected {
            t.Errorf("Expected body to contain value %q but got %q", expected, resultStr)
    }
}  

以上单元测试是不是就很简单了。

如何优雅的使用 Go Interface?

以上的样例其实都来自今天要推荐的开源项目。如果你非常关注架构和代码的整洁,以及代码的可测试性,非常推荐大家看一下。

更多项目详情请查看如下链接,尤其是项目中的代码,很简单但非常值得看一下。

开源项目地址:

文章来源:智云一二三科技

文章标题:Go Interface 的优雅使用,让代码更整洁更容易测试

文章地址:https://www.zhihuclub.com/87227.shtml

关于作者: 智云科技

热门文章

网站地图