├── README.md ├── api ├── handlers │ ├── article_handler.go │ └── article_handler_test.go └── router.go ├── app ├── db.go ├── gin_engine.go └── server.go ├── cmd ├── main.go ├── wire.go └── wire_gen.go ├── go.mod ├── mock ├── repo_mock.go └── service_mock.go ├── models └── article.go ├── repo ├── article_repo.go ├── article_repo_test.go └── repo.go └── service ├── article_service.go ├── article_service_test.go └── service.go /README.md: -------------------------------------------------------------------------------- 1 | 此代码对应文章:Golang 简洁架构实战 https://www.luozhiyun.com/archives/640 2 | -------------------------------------------------------------------------------- /api/handlers/article_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "my-clean-rchitecture/service" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | // ArticleHandler 13 | type ArticleHandler struct { 14 | ArticleService service.IArticleService 15 | } 16 | 17 | // NewArticleHandler 18 | func NewArticleHandler(as service.IArticleService) ArticleHandler { 19 | handler := ArticleHandler{ 20 | ArticleService: as, 21 | } 22 | return handler 23 | } 24 | 25 | // FetchArticle will fetch the article based on given params 26 | func (a *ArticleHandler) FetchArticle(c *gin.Context) { 27 | numS := c.Query("num") 28 | num, _ := strconv.Atoi(numS) 29 | createDate := c.Query("create_date") 30 | parseDate, _ := time.Parse("2006-01-02", createDate) 31 | listAr, err := a.ArticleService.Fetch(c, parseDate, num) 32 | if err != nil { 33 | fmt.Printf("error %v", err) 34 | return 35 | } 36 | c.JSON(http.StatusOK, gin.H{"articles": listAr}) 37 | } 38 | -------------------------------------------------------------------------------- /api/handlers/article_handler_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/golang/mock/gomock" 6 | "my-clean-rchitecture/mock" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestArticleHandler_FetchArticle(t *testing.T) { 14 | 15 | ctl := gomock.NewController(t) 16 | defer ctl.Finish() 17 | createAt, _ := time.Parse("2006-01-02", "2021-12-26") 18 | mockService := mock.NewMockIArticleService(ctl) 19 | 20 | gomock.InOrder( 21 | mockService.EXPECT().Fetch(gomock.Any(), createAt, 10).Return(nil, nil), 22 | ) 23 | 24 | article := NewArticleHandler(mockService) 25 | 26 | gin.SetMode(gin.TestMode) 27 | 28 | // Setup your router, just like you did in your main function, and 29 | // register your routes 30 | r := gin.Default() 31 | r.GET("/articles", article.FetchArticle) 32 | 33 | req, err := http.NewRequest(http.MethodGet, "/articles?num=10&create_date=2021-12-26", nil) 34 | if err != nil { 35 | t.Fatalf("Couldn't create request: %v\n", err) 36 | } 37 | 38 | w := httptest.NewRecorder() 39 | // Perform the request 40 | r.ServeHTTP(w, req) 41 | 42 | // Check to see if the response was what you expected 43 | if w.Code != http.StatusOK { 44 | t.Fatalf("Expected to get status %d but instead got %d\n", http.StatusOK, w.Code) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/router.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "my-clean-rchitecture/api/handlers" 6 | ) 7 | 8 | type Router struct { 9 | article handlers.ArticleHandler 10 | } 11 | 12 | func (r *Router) With(engine *gin.Engine) { 13 | engine.GET("/articles", r.article.FetchArticle) 14 | } 15 | 16 | func NewRouter(article handlers.ArticleHandler) *Router { 17 | router := &Router{ 18 | article: article, 19 | } 20 | return router 21 | } 22 | -------------------------------------------------------------------------------- /app/db.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | func InitDB() *gorm.DB { 8 | //dsn := "root:123456@tcp(127.0.0.1:3306)?charset=utf8mb4&parseTime=True&loc=Local" 9 | //db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 10 | //if err != nil { 11 | // log.Fatal(err) 12 | //} 13 | return &gorm.DB{} 14 | } 15 | -------------------------------------------------------------------------------- /app/gin_engine.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func NewGinEngine() *gin.Engine { 6 | return gin.Default() 7 | } 8 | -------------------------------------------------------------------------------- /app/server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "my-clean-rchitecture/api" 6 | ) 7 | 8 | type Server struct { 9 | engine *gin.Engine 10 | apiRouter *api.Router 11 | } 12 | 13 | func (s *Server) Start() { 14 | s.apiRouter.With(s.engine) 15 | s.engine.Run() 16 | } 17 | 18 | func NewServer(engine *gin.Engine, apiRouter *api.Router) *Server { 19 | return &Server{ 20 | engine: engine, 21 | apiRouter: apiRouter, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | server := InitServer() 5 | server.Start() 6 | } 7 | -------------------------------------------------------------------------------- /cmd/wire.go: -------------------------------------------------------------------------------- 1 | //+build wireinject 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/google/wire" 7 | "my-clean-rchitecture/api" 8 | "my-clean-rchitecture/api/handlers" 9 | "my-clean-rchitecture/app" 10 | "my-clean-rchitecture/repo" 11 | "my-clean-rchitecture/service" 12 | ) 13 | 14 | func InitServer() *app.Server { 15 | wire.Build( 16 | app.InitDB, 17 | repo.NewMysqlArticleRepository, 18 | service.NewArticleService, 19 | handlers.NewArticleHandler, 20 | api.NewRouter, 21 | app.NewServer, 22 | app.NewGinEngine) 23 | return &app.Server{} 24 | } 25 | -------------------------------------------------------------------------------- /cmd/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //+build !wireinject 5 | 6 | package main 7 | 8 | import ( 9 | "my-clean-rchitecture/api" 10 | "my-clean-rchitecture/api/handlers" 11 | "my-clean-rchitecture/app" 12 | "my-clean-rchitecture/repo" 13 | "my-clean-rchitecture/service" 14 | ) 15 | 16 | // Injectors from wire.go: 17 | 18 | func InitServer() *app.Server { 19 | engine := app.NewGinEngine() 20 | db := app.InitDB() 21 | iArticleRepo := repo.NewMysqlArticleRepository(db) 22 | iArticleService := service.NewArticleService(iArticleRepo) 23 | articleHandler := handlers.NewArticleHandler(iArticleService) 24 | router := api.NewRouter(articleHandler) 25 | server := app.NewServer(engine, router) 26 | return server 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module my-clean-rchitecture 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/gin-gonic/gin v1.7.7 8 | github.com/golang/mock v1.6.0 9 | github.com/google/wire v0.5.0 10 | github.com/stretchr/testify v1.7.0 // indirect 11 | gorm.io/driver/mysql v1.2.2 12 | gorm.io/gorm v1.22.4 13 | ) 14 | -------------------------------------------------------------------------------- /mock/repo_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: .\repo\repo.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "my-clean-rchitecture/models" 10 | reflect "reflect" 11 | time "time" 12 | 13 | gomock "github.com/golang/mock/gomock" 14 | ) 15 | 16 | // MockIArticleRepo is a mock of IArticleRepo interface. 17 | type MockIArticleRepo struct { 18 | ctrl *gomock.Controller 19 | recorder *MockIArticleRepoMockRecorder 20 | } 21 | 22 | // MockIArticleRepoMockRecorder is the mock recorder for MockIArticleRepo. 23 | type MockIArticleRepoMockRecorder struct { 24 | mock *MockIArticleRepo 25 | } 26 | 27 | // NewMockIArticleRepo creates a new mock instance. 28 | func NewMockIArticleRepo(ctrl *gomock.Controller) *MockIArticleRepo { 29 | mock := &MockIArticleRepo{ctrl: ctrl} 30 | mock.recorder = &MockIArticleRepoMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockIArticleRepo) EXPECT() *MockIArticleRepoMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Fetch mocks base method. 40 | func (m *MockIArticleRepo) Fetch(ctx context.Context, createdDate time.Time, num int) ([]models.Article, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Fetch", ctx, createdDate, num) 43 | ret0, _ := ret[0].([]models.Article) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Fetch indicates an expected call of Fetch. 49 | func (mr *MockIArticleRepoMockRecorder) Fetch(ctx, createdDate, num interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIArticleRepo)(nil).Fetch), ctx, createdDate, num) 52 | } 53 | -------------------------------------------------------------------------------- /mock/service_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: .\service\service.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "my-clean-rchitecture/models" 10 | reflect "reflect" 11 | time "time" 12 | 13 | gomock "github.com/golang/mock/gomock" 14 | ) 15 | 16 | // MockIArticleService is a mock of IArticleService interface. 17 | type MockIArticleService struct { 18 | ctrl *gomock.Controller 19 | recorder *MockIArticleServiceMockRecorder 20 | } 21 | 22 | // MockIArticleServiceMockRecorder is the mock recorder for MockIArticleService. 23 | type MockIArticleServiceMockRecorder struct { 24 | mock *MockIArticleService 25 | } 26 | 27 | // NewMockIArticleService creates a new mock instance. 28 | func NewMockIArticleService(ctrl *gomock.Controller) *MockIArticleService { 29 | mock := &MockIArticleService{ctrl: ctrl} 30 | mock.recorder = &MockIArticleServiceMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockIArticleService) EXPECT() *MockIArticleServiceMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Fetch mocks base method. 40 | func (m *MockIArticleService) Fetch(ctx context.Context, createdDate time.Time, num int) ([]models.Article, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Fetch", ctx, createdDate, num) 43 | ret0, _ := ret[0].([]models.Article) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Fetch indicates an expected call of Fetch. 49 | func (mr *MockIArticleServiceMockRecorder) Fetch(ctx, createdDate, num interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIArticleService)(nil).Fetch), ctx, createdDate, num) 52 | } 53 | -------------------------------------------------------------------------------- /models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Article struct { 8 | ID int64 `json:"id"` 9 | Title string `json:"title" validate:"required"` 10 | Content string `json:"content" validate:"required"` 11 | UpdatedAt time.Time `json:"updated_at"` 12 | CreatedAt time.Time `json:"created_at"` 13 | } 14 | 15 | -------------------------------------------------------------------------------- /repo/article_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "gorm.io/gorm" 6 | "my-clean-rchitecture/models" 7 | "time" 8 | ) 9 | 10 | type mysqlArticleRepository struct { 11 | DB *gorm.DB 12 | } 13 | 14 | // NewMysqlArticleRepository will create an object that represent the article.Repository interface 15 | func NewMysqlArticleRepository(DB *gorm.DB) IArticleRepo { 16 | return &mysqlArticleRepository{DB} 17 | } 18 | 19 | func (m *mysqlArticleRepository) Fetch(ctx context.Context, createdDate time.Time, 20 | num int) (res []models.Article, err error) { 21 | 22 | err = m.DB.WithContext(ctx).Model(&models.Article{}). 23 | Select("id,title,content, updated_at, created_at"). 24 | Where("created_at > ?", createdDate).Limit(num).Find(&res).Error 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /repo/article_repo_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "github.com/DATA-DOG/go-sqlmock" 7 | "github.com/stretchr/testify/assert" 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | "log" 11 | "my-clean-rchitecture/models" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func getSqlMock() (mock sqlmock.Sqlmock, gormDB *gorm.DB) { 17 | //创建sqlmock 18 | var err error 19 | var db *sql.DB 20 | db, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 21 | if err != nil { 22 | panic(err) 23 | } 24 | //结合gorm、sqlmock 25 | gormDB, err = gorm.Open(mysql.New(mysql.Config{ 26 | SkipInitializeWithVersion: true, 27 | Conn: db, 28 | }), &gorm.Config{}) 29 | if nil != err { 30 | log.Fatalf("Init DB with sqlmock failed, err %v", err) 31 | } 32 | return 33 | } 34 | 35 | func Test_mysqlArticleRepository_Fetch(t *testing.T) { 36 | createAt := time.Now() 37 | updateAt := time.Now() 38 | //id,title,content, updated_at, created_at 39 | var articles = []models.Article{ 40 | {1, "test1", "content", updateAt, createAt}, 41 | {2, "test2", "content2", updateAt, createAt}, 42 | } 43 | 44 | limit := 2 45 | mock, db := getSqlMock() 46 | 47 | mock.ExpectQuery("SELECT id,title,content, updated_at, created_at FROM `articles` WHERE created_at > ? LIMIT 2"). 48 | WithArgs(createAt). 49 | WillReturnRows(sqlmock.NewRows([]string{"id", "title", "content", "updated_at", "created_at"}). 50 | AddRow(articles[0].ID, articles[0].Title, articles[0].Content, articles[0].UpdatedAt, articles[0].CreatedAt). 51 | AddRow(articles[1].ID, articles[1].Title, articles[1].Content, articles[1].UpdatedAt, articles[1].CreatedAt)) 52 | 53 | repository := NewMysqlArticleRepository(db) 54 | result, err := repository.Fetch(context.TODO(), createAt, limit) 55 | 56 | assert.Nil(t, err) 57 | assert.Equal(t, articles, result) 58 | } 59 | -------------------------------------------------------------------------------- /repo/repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "my-clean-rchitecture/models" 6 | "time" 7 | ) 8 | 9 | // IArticleRepo represent the article's repository contract 10 | type IArticleRepo interface { 11 | Fetch(ctx context.Context, createdDate time.Time, num int) (res []models.Article, err error) 12 | } 13 | -------------------------------------------------------------------------------- /service/article_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "my-clean-rchitecture/models" 6 | "my-clean-rchitecture/repo" 7 | "time" 8 | ) 9 | 10 | type articleService struct { 11 | articleRepo repo.IArticleRepo 12 | } 13 | 14 | // NewArticleService will create new an articleUsecase object representation of domain.ArticleUsecase interface 15 | func NewArticleService(a repo.IArticleRepo) IArticleService { 16 | return &articleService{ 17 | articleRepo: a, 18 | } 19 | } 20 | 21 | // Fetch 22 | func (a *articleService) Fetch(ctx context.Context, createdDate time.Time, num int) (res []models.Article, err error) { 23 | if num == 0 { 24 | num = 10 25 | } 26 | res, err = a.articleRepo.Fetch(ctx, createdDate, num) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /service/article_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/golang/mock/gomock" 7 | "my-clean-rchitecture/mock" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func Test_articleService_Fetch(t *testing.T) { 13 | ctl := gomock.NewController(t) 14 | defer ctl.Finish() 15 | now := time.Now() 16 | mockRepo := mock.NewMockIArticleRepo(ctl) 17 | 18 | gomock.InOrder( 19 | mockRepo.EXPECT().Fetch(context.TODO(), now, 10).Return(nil, nil), 20 | ) 21 | 22 | service := NewArticleService(mockRepo) 23 | 24 | fetch, _ := service.Fetch(context.TODO(), now, 10) 25 | fmt.Println(fetch) 26 | } 27 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "my-clean-rchitecture/models" 6 | "time" 7 | ) 8 | 9 | // IArticleService represent the article's usecases 10 | type IArticleService interface { 11 | Fetch(ctx context.Context, createdDate time.Time, num int) ([]models.Article, error) 12 | } 13 | --------------------------------------------------------------------------------