go源码使用教程:七爪源码让我们使用
go源码使用教程:七爪源码让我们使用服务:这是我们的业务逻辑所在的层。Handler:它是一个获取http请求并将http响应返回给客户端的层。让我们看看下面这张图。 看起来很简单,对吧?这种架构背后的逻辑每一层都有自己的责任,它们将相互独立。这样,我们可以以更独立的方式测试它们。是的,再次测试!这些层(处理程序、服务和存储库)的作用是什么?
软件架构的目标是最大限度地减少构建和维护所需系统所需的人力资源。
我们将继续前进,看看如何通过三层架构更轻松地测试我们的电影 API。
分层架构的想法建立在对接口进行编程的想法之上。 当一个模块通过接口与另一个模块交互时,您可以将一个服务提供者替换为另一个。
不用担心! 我会尽力简单地解释。
让我们看看下面这张图。 看起来很简单,对吧?
这种架构背后的逻辑每一层都有自己的责任,它们将相互独立。这样,我们可以以更独立的方式测试它们。是的,再次测试!
这些层(处理程序、服务和存储库)的作用是什么?
Handler:它是一个获取http请求并将http响应返回给客户端的层。
服务:这是我们的业务逻辑所在的层。
存储库:它是从外部(DB)或内部(内存)数据源提供所有必要数据的层。为简单起见,我们使用内存。
在学习图中接口的作用之前,我们需要先谈谈依赖注入。依赖注入仅基于抽象(接口)而不是具体类型(结构)。以这种方式,我们通过使用接口注入所有依赖项。
比如我们在项目中会有两个服务:MockService和DefaultService。借助服务接口,我们可以在Handler层使用这两个不同结构体的方法。在测试阶段,Handler 与 MockService 交互,另一方面,它与生产中的 DefaultService 交互。
不用担心,当我们看到动作时,您会更好地理解:
type movieHandler struct {
service service.IMovieService
}
func NewMovieHandler(ms service.IMovieService) *movieHandler {
return &movieHandler{service: ms}
}
//curl -X PATCH "localhost:8080/movies/1" -d '{ "title": "Beautiful film" }'
func (mh *movieHandler) UpdateMovie(w http.ResponseWriter r *http.request ps httprouter.Params) {
// some piece of code
err = mh.service.UpdateMovie(id movie)
// some piece of code
}
type DefaultMovieService struct {
movieRepo repository.IMovieRepository
}
func NewDefaultMovieService(mRepo repository.IMovieRepository) *DefaultMovieService {
return &DefaultMovieService{
movieRepo: mRepo
}
}
func (d *DefaultMovieService) UpdateMovie(id int movie model.Movie) error {
// some piece of code
err := d.movieRepo.UpdateMovie(id movie)
// some piece of code
}
type inmemoryMovieRepository struct {
Movies []model.Movie
}
func NewInMemoryMovieRepository() *inmemoryMovieRepository {
var movies = []model.Movie{
{ID: 1 Title: "The Shawshank Redemption" ReleaseYear: 1994 Score: 9.3}
{ID: 2 Title: "The Godfather" ReleaseYear: 1972 Score: 9.2}
{ID: 3 Title: "The Dark Knight" ReleaseYear: 2008 Score: 9.0}
}
return &inmemoryMovieRepository{
Movies: movies
}
}
func (i *inmemoryMovieRepository) UpdateMovie(id int movie model.Movie) error {
// some piece of code
}
实际上,当我们调用 Handler 中的 service 方法和 Service 中的 repository 方法时,将这种关系称为“调用堆栈”是合理的。
这是我们使用调用堆栈所做的简单图。我们的计算机为我们的函数调用分配内存。
让我们把这段记忆想象成一个盒子。
我们的第一个调用(在 Handler 中)需要保存在内存中。
然后,我们的第二个调用(在服务中)被保存在处理程序框的内存中。
然后,我们的第三个调用(在 Repository 中)被保存在 Service 框的内存中。棘手的是我们的服务需要等待存储库功能完成。完成后,我们的处理程序需要等待服务功能完成。完成后,Handler 函数就可以完成它的工作了。
完成后,我们的堆栈将是空的。这就是调用堆栈的工作方式。您可以阅读“Grokking 算法书的调用堆栈”部分以了解更多信息。
这是我们如何创建设计的广泛视角。我们可以深入到我们的项目中。我们将与本文一起处理一个 PATCH 请求示例。
1. main.go
当客户端使用 PATCH 方法发送请求时,movieHandler 的 UpdateMovie 方法使用 httprouter 调用。
package main
import (
"github/dilaragorum/movie-go/handler"
"github/dilaragorum/movie-go/repository"
"github/dilaragorum/movie-go/service"
"github/julienschmidt/httprouter"
"log"
"net/http"
)
func main() {
movieInMemoryRepository := repository.NewInMemoryMovieRepository()
movieService := service.NewDefaultMovieService(movieInMemoryRepository)
movieHandler := handler.NewMovieHandler(movieService)
router := httprouter.New()
router.PATCH("/movies/:id" movieHandler.UpdateMovie)
log.Println("http server runs on :8080")
err := httpstenAndServe(":8080" router)
log.Fatal(err)
}
2a. 处理程序
这是我们的 API 获取 HTTP 请求的第一层。
package handler
import (
"encoding/json"
"errors"
"github/dilaragorum/movie-go/model"
"github/dilaragorum/movie-go/service"
"github/julienschmidt/httprouter"
"net/http"
"strconv"
)
type movieHandler struct {
service service.IMovieService
}
func NewMovieHandler(ms service.IMovieService) *movieHandler {
Return &movieHandler{service: ms}
}
// curl -X PATCH "localhost:8080/movies/1" -d '{ "title": "Beautiful film" }'
func (mh *movieHandler) UpdateMovie(w http.ResponseWriter r *http.Request ps httprouter.Params) {
id _ := strconv.Atoi(ps.ByName("id"))
var movie model.Movie
err := json.NewDecoder(r.Body).Decode(&movie)
if err != nil {
http.Error(w "error when decoding json" http.StatusInternalServerError)
return
}
err = mh.service.UpdateMovie(id movie)
if err != nil {
if errors.Is(err service.ErrIDIsNotValid) ||
errors.Is(err service.ErrTitleIsNotEmpty) {
http.Error(w err.Error() http.StatusBadRequest)
return
} else if errors.Is(err service.ErrMovieNotFound) {
http.Error(w err.Error() http.StatusNotFound)
return
}
http.Error(w err.Error() http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
w.Write([]byte("Successfully Updated"))
}
处理程序是将来自服务的响应转换为http响应的层。 如您所知,HTTP 响应由状态码、标头和正文组成。
2b。 处理程序测试
在继续测试处理程序之前,我想介绍 mockgen 包。 这个包帮助我们的代码轻松测试。
给它你想要模拟的接口源路径,显示自动生成的模拟文件将在哪里创建,并让它代表你生成模拟结构实现。 如果我们想测试处理层,我们需要模拟服务层。 我们可以像这样安装它:
mockgen -source service/movie_service_interface.go -destination service/mock_movie_service.go -package service
在测试文件中,我们应该测试可能的错误和成功案例,以增加处理程序的测试覆盖率。
func TestMovieHandler_UpdateMovie(t *testing.T) {
movieID := "1"
requestURL := fmt.Sprintf("/movies/%s" movieID)
updatedMovie := model.Movie{Title: "Test Movie"}
jsonStr _ := json.Marshal(updatedMovie)
ps := httprouter.Params{
{Key: "id" Value: movieID}
}
t.Run("update movie error - Bad Request" func(t *testing.T) {
type testCase struct {
returnedServiceErr error
expectedHTTPStatusCode int
}
testErrors := []testCase{
{returnedServiceErr: service.ErrIDIsNotValid expectedHTTPStatusCode: http.StatusBadRequest}
{returnedServiceErr: service.ErrTitleIsNotEmpty expectedHTTPStatusCode: http.StatusBadRequest}
}
for _ testerror := range testErrors {
req _ := http.NewRequest(httpthodPatch requestURL bytes.NewBuffer(jsonStr))
rec := httptest.NewRecorder()
mockService := service.NewMockIMovieService(gomock.NewController(t))
mockService.
EXPECT().
UpdateMovie(1 updatedMovie).
Return(testError.returnedServiceErr).
Times(1)
mh := NewMovieHandler(mockService)
mh.UpdateMovie(rec req ps)
assert.Equal(t testError.expectedHTTPStatusCode rec.Code)
}
})
t.Run("update movie error - Status Not Found Error" func(t *testing.T) {
req _ := http.NewRequest(httpthodPatch requestURL bytes.NewBuffer(jsonStr))
rec := httptest.NewRecorder()
mockService := service.NewMockIMovieService(gomock.NewController(t))
mockService.
EXPECT().
UpdateMovie(1 updatedMovie).
Return(service.ErrMovieNotFound).
Times(1)
mh := NewMovieHandler(mockService)
mh.UpdateMovie(rec req ps)
assert.Equal(t http.StatusNotFound rec.Code)
})
t.Run("update movie error - Internal Server Error" func(t *testing.T) {
req _ := http.NewRequest(httpthodPatch requestURL bytes.NewBuffer(jsonStr))
rec := httptest.NewRecorder()
mockService := service.NewMockIMovieService(gomock.NewController(t))
mockService.
EXPECT().
UpdateMovie(1 updatedMovie).
Return(errors.New("")).
Times(1)
mh := NewMovieHandler(mockService)
mh.UpdateMovie(rec req ps)
assert.Equal(t http.StatusInternalServerError rec.Code)
})
t.Run("update movie successfully" func(t *testing.T) {
req _ := http.NewRequest(httpthodPatch requestURL bytes.NewBuffer(jsonStr))
rec := httptest.NewRecorder()
mockService := service.NewMockIMovieService(gomock.NewController(t))
mockService.
EXPECT().
UpdateMovie(1 updatedMovie).
Return(nil).
Times(1)
mh := NewMovieHandler(mockService)
mh.UpdateMovie(rec req ps)
assert.Equal(t http.StatusNoContent rec.Code)
assert.Equal(t "Successfully Updated" rec.Body.String())
})
}
3a。 服务
这一层包括我们的应用业务逻辑。 通过将业务逻辑分离到特定的层,我们可以轻松编写单元测试。
package service
import (
"errors"
"github/dilaragorum/movie-go/model"
"github/dilaragorum/movie-go/repository"
)
var (
ErrIDIsNotValid = errors.New("id is not valid")
ErrTitleIsNotEmpty = errors.New("Movie title cannot be empty")
ErrMovieNotFound = errors.New("the movie cannot be found")
)
type DefaultMovieService struct {
movieRepo repository.IMovieRepository
}
func NewDefaultMovieService(mRepo repository.IMovieRepository) *DefaultMovieService {
return &DefaultMovieService{
movieRepo: mRepo
}
}
func (d *DefaultMovieService) UpdateMovie(id int movie model.Movie) error {
if id <= 0 {
return ErrIDIsNotValid
}
if movie.Title == "" {
return ErrTitleIsNotEmpty
}
err := d.movieRepo.UpdateMovie(id movie)
if errors.Is(err repository.ErrMovieNotFound) {
return ErrMovieNotFound
}
return nil
}
3b。 服务测试
就像在处理程序测试部分一样,我们需要模拟我们的存储库来测试我们的服务层:
mockgen -source repository/movie_repository_interface.go -destination repository/mock_movie_repository.go -package repository
我会给你一个窍门。 每当我们更改提供给 mockgen 的接口时,我们都必须再次运行 mockgen 命令。
到目前为止,我们有两个模拟文件,当我们向接口添加新方法时,我们需要一遍又一遍地编写这些长代码。 这么乏味的方式! 解决方案是 Makefile。 使用 Makefile,我们只使用 generate-mocks 命令来重新创建我们的模拟文件。(make generate-mocks)
我们正在通过模拟存储库来测试可能的错误。
func TestDefaultMovieService_UpdateMovie(t *testing.T) {
t.Run("Error Update Movie - IDIsNotValid" func(t *testing.T) {
ms := NewDefaultMovieService(nil)
err := ms.UpdateMovie(0 model.Movie{Title: ""})
assert.ErrorIs(t err ErrIDIsNotValid)
})
t.Run("Error Update Movie - ErrTitleIsNotEmpty" func(t *testing.T) {
ms := NewDefaultMovieService(nil)
err := ms.UpdateMovie(3 model.Movie{Title: ""})
assert.ErrorIs(t err ErrTitleIsNotEmpty)
})
t.Run("Error Update Movie - ErrMovieNotFound" func(t *testing.T) {
movie := model.Movie{Title: "Test Movie"}
mockRepository := repository.NewMockIMovieRepository(gomock.NewController(t))
mockRepository.
EXPECT().
UpdateMovie(6 movie).
Return(repository.ErrMovieNotFound).
Times(1)
ms := NewDefaultMovieService(mockRepository)
err := ms.UpdateMovie(6 movie)
assert.ErrorIs(t err ErrMovieNotFound)
})
t.Run("Error Update Movie - ErrMovieNotFound" func(t *testing.T) {
movie := model.Movie{Title: "Test Movie"}
mockRepository := repository.NewMockIMovieRepository(gomock.NewController(t))
mockRepository.
EXPECT().
UpdateMovie(2 movie).
Return(nil).
Times(1)
ms := NewDefaultMovieService(mockRepository)
err := ms.UpdateMovie(2 movie)
assert.Nil(t err)
})
}
4. 存储库
这是我们实现数据集成的地方:
package repository
import (
"errors"
"github/dilaragorum/movie-go/model"
)
var (
ErrMovieNotFound = errors.New("FromRepository - movie not found")
)
type inmemoryMovieRepository struct {
Movies []model.Movie
}
func NewInMemoryMovieRepository() *inmemoryMovieRepository {
var movies = []model.Movie{
{ID: 1 Title: "The Shawshank Redemption" ReleaseYear: 1994 Score: 9.3}
{ID: 2 Title: "The Godfather" ReleaseYear: 1972 Score: 9.2}
{ID: 3 Title: "The Dark Knight" ReleaseYear: 2008 Score: 9.0}
}
return &inmemoryMovieRepository{
Movies: movies
}
}
func (i *inmemoryMovieRepository) UpdateMovie(id int movie model.Movie) error {
for k := 0; k < len(i.Movies); k {
if i.Movies[k].ID == id {
i.Movies[k].Title = movie.Title
return nil
}
}
return ErrMovieNotFound
}
奖励:HTTP 客户端插件
我们正在尝试开发 RESTful API,并且在开发过程中,我们希望确保它按预期工作。 可以肯定的是,我们需要向我们的 API 发送 HTTP 请求。
你可以使用 curl、Postman、Insomnia 和 Jetbrains HTTP Client 插件等两种方式来做到这一点。我真的很喜欢使用 Jetbrains HTTP Client 插件。
使用 HTTP Client 插件,您可以直接在 IntelliJ IDEA 代码编辑器中创建、编辑和执行 HTTP 请求。
关注七爪网,获取更多APP/小程序/网站源码资源!