├── .gitignore ├── README.md ├── errors └── error.go ├── go.mod ├── go.sum ├── main.go ├── models └── book.go ├── server ├── decoder │ └── decoder.go ├── handlers │ ├── book │ │ ├── entity.go │ │ └── handler.go │ └── handler.go ├── inmemory.go ├── mongo.go ├── responses │ └── response.go └── server.go └── services └── book ├── repository ├── inmemory.go ├── mongo.go └── repository.go └── service.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-web-server-tips 2 | 3 | This project demonstrates some code practices I use when writing web servers in Go. 4 | 5 | To start, run: 6 | 7 | ```shell script 8 | go run main.go 9 | ``` 10 | 11 | Blog post: [dev.to/chidiwilliams/writing-cleaner-go-web-servers-3oe4](https://dev.to/chidiwilliams/writing-cleaner-go-web-servers-3oe4) 12 | 13 | ## Tips 14 | 15 | - [x] Use clean architecture 16 | 17 | - Good code organization/folder structure 18 | - Decouple dependencies 19 | 20 | - [x] Extend HTTP handler 21 | 22 | - Handle errors in one location 23 | 24 | - [x] Standardized response format 25 | 26 | - [x] Create custom errors for client errors 27 | 28 | - Clean error handling 29 | 30 | - [ ] ozzo-validator with custom validator? 31 | 32 | - Struct validation outside controller 33 | 34 | - [ ] Integration testing with testify? 35 | -------------------------------------------------------------------------------- /errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // Type describes the kind of error and roughly translates 4 | // to a HTTP status code for client errors. 5 | type Type string 6 | 7 | const ( 8 | // TypeBadRequest is used for HTTP 400-like errors. 9 | TypeBadRequest Type = "bad_request_error" 10 | // TypeNotFound is used for HTTP 404-like errors. 11 | TypeNotFound Type = "not_found_error" 12 | ) 13 | 14 | // AppError is an implementation of error with types to 15 | // differentiate client and server errors. 16 | type AppError struct { 17 | text string 18 | errType Type 19 | } 20 | 21 | func (e AppError) Error() string { 22 | return e.text 23 | } 24 | 25 | // Type returns the type of the error. 26 | func (e AppError) Type() Type { 27 | return e.errType 28 | } 29 | 30 | // Error returns an AppError with a TypeBadRequest type. 31 | func Error(text string) error { 32 | return &AppError{ 33 | text: text, 34 | errType: TypeBadRequest, 35 | } 36 | } 37 | 38 | // NotFound returns an AppError with a TypeNotFound type. 39 | func NotFound(entity string) error { 40 | return &AppError{ 41 | text: entity + " not found", 42 | errType: TypeNotFound, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chidiwilliams/go-web-server-tips 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/mux v1.7.4 7 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 8 | github.com/stretchr/testify v1.5.1 // indirect 9 | github.com/tidwall/buntdb v1.1.2 10 | github.com/tidwall/gjson v1.6.0 // indirect 11 | github.com/tidwall/pretty v1.0.1 // indirect 12 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 13 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 14 | gopkg.in/yaml.v2 v2.2.8 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 4 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 9 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 14 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 15 | github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= 16 | github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= 17 | github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= 18 | github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= 19 | github.com/tidwall/gjson v1.3.4 h1:On5waDnyKKk3SWE4EthbjjirAWXp43xx5cKCUZY1eZw= 20 | github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 21 | github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= 22 | github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 23 | github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= 24 | github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= 25 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= 26 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 27 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 28 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 29 | github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= 30 | github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 31 | github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= 32 | github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= 33 | github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= 34 | github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 37 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= 39 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 40 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 41 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 42 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/chidiwilliams/go-web-server-tips/server" 7 | ) 8 | 9 | func main() { 10 | srv := server.Server() 11 | log.Println("Server listening on", srv.Addr) 12 | log.Fatal(srv.ListenAndServe()) 13 | } 14 | -------------------------------------------------------------------------------- /models/book.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/mgo.v2/bson" 7 | ) 8 | 9 | type Book struct { 10 | ID bson.ObjectId `json:"id,omitempty" bson:"_id,omitempty"` 11 | Title string `json:"title,omitempty" bson:"title,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /server/decoder/decoder.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "github.com/chidiwilliams/go-web-server-tips/errors" 8 | ) 9 | 10 | // DecodeJSON reads JSON data from reader and decodes it 11 | // into the value pointed to by v. 12 | func DecodeJSON(r io.Reader, v interface{}) error { 13 | if err := json.NewDecoder(r).Decode(v); err != nil { 14 | return errors.Error(err.Error()) 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /server/handlers/book/entity.go: -------------------------------------------------------------------------------- 1 | package book 2 | 3 | import "github.com/chidiwilliams/go-web-server-tips/models" 4 | 5 | type getBookResponse struct { 6 | Book *models.Book `json:"book"` 7 | } 8 | 9 | type createBookResponse struct { 10 | Book *models.Book `json:"book"` 11 | } 12 | -------------------------------------------------------------------------------- /server/handlers/book/handler.go: -------------------------------------------------------------------------------- 1 | package book 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | "gopkg.in/mgo.v2/bson" 8 | 9 | "github.com/chidiwilliams/go-web-server-tips/errors" 10 | "github.com/chidiwilliams/go-web-server-tips/server/decoder" 11 | "github.com/chidiwilliams/go-web-server-tips/server/responses" 12 | "github.com/chidiwilliams/go-web-server-tips/services/book" 13 | ) 14 | 15 | type Handler interface { 16 | CreateBook(w http.ResponseWriter, r *http.Request) error 17 | GetBook(w http.ResponseWriter, r *http.Request) error 18 | } 19 | 20 | func NewBookHandler(bookService book.Service) Handler { 21 | return handler{bookService} 22 | } 23 | 24 | type handler struct { 25 | bookService book.Service 26 | } 27 | 28 | type createBookRequestBody struct { 29 | Title string `json:"title"` 30 | } 31 | 32 | func (u handler) CreateBook(w http.ResponseWriter, r *http.Request) error { 33 | requestBody := &createBookRequestBody{} 34 | if err := decoder.DecodeJSON(r.Body, requestBody); err != nil { 35 | return err 36 | } 37 | 38 | newBook, err := u.bookService.CreateBook(requestBody.Title) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return responses.OK("We've added your book!", createBookResponse{Book: newBook}).ToJSON(w) 44 | } 45 | 46 | func (u handler) GetBook(w http.ResponseWriter, r *http.Request) error { 47 | id := mux.Vars(r)["bookID"] 48 | if !bson.IsObjectIdHex(id) { 49 | return errors.Error("invalid vendor ID") 50 | } 51 | 52 | retrievedBook, err := u.bookService.GetBook(bson.ObjectIdHex(id)) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return responses.OK("We found your book!", getBookResponse{retrievedBook}).ToJSON(w) 58 | } 59 | -------------------------------------------------------------------------------- /server/handlers/handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | errors2 "github.com/chidiwilliams/go-web-server-tips/errors" 10 | "github.com/chidiwilliams/go-web-server-tips/server/responses" 11 | ) 12 | 13 | // Handler is an implementation of http.Handler with a handler 14 | // function that returns an error 15 | type Handler func(w http.ResponseWriter, r *http.Request) error 16 | 17 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | err := h(w, r) 19 | if err == nil { 20 | return 21 | } 22 | 23 | if err = respondWithErr(err, w); err != nil { 24 | log.Println("error writing http response:", err) 25 | } 26 | } 27 | 28 | func respondWithErr(err error, w http.ResponseWriter) error { 29 | appError := new(errors2.AppError) 30 | if errors.As(err, &appError) { // client error 31 | return responses.Fail( 32 | toSentenceCase(err.Error()), 33 | errTypeStatusCode(appError.Type()), 34 | ).ToJSON(w) 35 | } 36 | 37 | log.Println("server error:", err) 38 | return responses.Fail("Internal Server Error", http.StatusInternalServerError).ToJSON(w) 39 | } 40 | 41 | func errTypeStatusCode(errType errors2.Type) int { 42 | switch errType { 43 | case errors2.TypeBadRequest: 44 | return http.StatusBadRequest 45 | case errors2.TypeNotFound: 46 | return http.StatusNotFound 47 | default: 48 | return http.StatusBadRequest 49 | } 50 | } 51 | 52 | func toSentenceCase(s string) string { 53 | if s == "" { 54 | return "" 55 | } 56 | return strings.ToUpper(string(s[0])) + s[1:] 57 | } 58 | -------------------------------------------------------------------------------- /server/inmemory.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/tidwall/buntdb" 5 | ) 6 | 7 | func connectToInMemoryDB() (*buntdb.DB, error) { 8 | return buntdb.Open(":memory:") 9 | } 10 | -------------------------------------------------------------------------------- /server/mongo.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | ) 6 | 7 | var mongoIndexes = map[string][]mgo.Index{} 8 | 9 | func connectToMongo() (*mgo.Database, error) { 10 | session, err := mgo.Dial("mongodb://localhost:27017/") 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return session.DB("go"), nil 16 | } 17 | 18 | func ensureMongoIndexes(db *mgo.Database) error { 19 | for coll, indexes := range mongoIndexes { 20 | for _, index := range indexes { 21 | err := db.C(coll).EnsureIndex(index) 22 | if err != nil { 23 | return err 24 | } 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /server/responses/response.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type response struct { 9 | Body *responseBody 10 | StatusCode int 11 | } 12 | 13 | type responseBody struct { 14 | Success bool `json:"success"` 15 | Message string `json:"message,omitempty"` 16 | Data interface{} `json:"data,omitempty"` 17 | } 18 | 19 | // ToJSON writes the response to the given http.ResponseWriter 20 | // with an application/json Content-Type header. 21 | func (r response) ToJSON(w http.ResponseWriter) error { 22 | w.Header().Set("Content-Type", "application/json") 23 | w.WriteHeader(r.StatusCode) 24 | return json.NewEncoder(w).Encode(r.Body) 25 | } 26 | 27 | // OK returns a new successful response. 28 | func OK(message string, data interface{}) *response { 29 | return newResponse(true, message, data, http.StatusOK) 30 | } 31 | 32 | // Fail returns a new failed response. 33 | func Fail(message string, statusCode int) *response { 34 | return newResponse(false, message, nil, statusCode) 35 | } 36 | 37 | func newResponse(success bool, message string, data interface{}, statusCode int) *response { 38 | return &response{ 39 | Body: &responseBody{ 40 | Success: success, 41 | Message: message, 42 | Data: data, 43 | }, 44 | StatusCode: statusCode, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | 9 | "github.com/chidiwilliams/go-web-server-tips/server/handlers" 10 | book2 "github.com/chidiwilliams/go-web-server-tips/server/handlers/book" 11 | "github.com/chidiwilliams/go-web-server-tips/services/book" 12 | "github.com/chidiwilliams/go-web-server-tips/services/book/repository" 13 | ) 14 | 15 | var ( 16 | bookHandler book2.Handler 17 | ) 18 | 19 | // Server configures and returns a new http.Server 20 | func Server() *http.Server { 21 | r := mux.NewRouter() 22 | 23 | r.Handle("/book", handlers.Handler(bookHandler.CreateBook)).Methods(http.MethodPost) 24 | r.Handle("/book/{bookID}", handlers.Handler(bookHandler.GetBook)).Methods(http.MethodGet) 25 | 26 | srv := &http.Server{Handler: r, Addr: ":8080"} 27 | return srv 28 | } 29 | 30 | func init() { 31 | inMemoryDB, err := connectToInMemoryDB() 32 | fatalIfErr(err) 33 | 34 | mongoDB, err := connectToMongo() 35 | fatalIfErr(err) 36 | 37 | err = ensureMongoIndexes(mongoDB) 38 | fatalIfErr(err) 39 | 40 | // Switch repository to Mongo with: 41 | // _ = repository.NewInMemoryRepository(inMemoryDB) 42 | // bookRepository := repository.NewMongoRepository(mongoDB) 43 | bookRepository := repository.NewInMemoryRepository(inMemoryDB) 44 | _ = repository.NewMongoRepository(mongoDB) 45 | 46 | bookService := book.NewService(bookRepository) 47 | bookHandler = book2.NewBookHandler(bookService) 48 | } 49 | 50 | func fatalIfErr(err error) { 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /services/book/repository/inmemory.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/tidwall/buntdb" 8 | "gopkg.in/mgo.v2/bson" 9 | 10 | "github.com/chidiwilliams/go-web-server-tips/models" 11 | ) 12 | 13 | func NewInMemoryRepository(db *buntdb.DB) Repository { 14 | return inMemoryRepository{db} 15 | } 16 | 17 | type inMemoryRepository struct { 18 | db *buntdb.DB 19 | } 20 | 21 | func (r inMemoryRepository) CreateBook(book models.Book) error { 22 | return r.db.Update(func(tx *buntdb.Tx) error { 23 | b, err := json.Marshal(&book) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | _, _, err = tx.Set(r.bookKey(book.ID), string(b), nil) 29 | return err 30 | }) 31 | } 32 | 33 | func (r inMemoryRepository) GetBook(id bson.ObjectId) (*models.Book, error) { 34 | var book *models.Book 35 | 36 | err := r.db.View(func(tx *buntdb.Tx) error { 37 | val, err := tx.Get(r.bookKey(id)) 38 | if err != nil { 39 | if err == buntdb.ErrNotFound { 40 | return errBookNotFound 41 | } 42 | return err 43 | } 44 | 45 | return json.Unmarshal([]byte(val), &book) 46 | }) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return book, nil 52 | } 53 | 54 | func (r inMemoryRepository) bookKey(id bson.ObjectId) string { 55 | return fmt.Sprintf("books::%s", id.Hex()) 56 | } 57 | -------------------------------------------------------------------------------- /services/book/repository/mongo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | 7 | "github.com/chidiwilliams/go-web-server-tips/models" 8 | ) 9 | 10 | func NewMongoRepository(db *mgo.Database) Repository { 11 | return mongoRepository{coll: db.C("books")} 12 | } 13 | 14 | type mongoRepository struct { 15 | coll *mgo.Collection 16 | } 17 | 18 | func (m mongoRepository) CreateBook(book models.Book) error { 19 | if err := m.coll.Insert(book); err != nil { 20 | return err 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func (m mongoRepository) GetBook(id bson.ObjectId) (*models.Book, error) { 27 | book := new(models.Book) 28 | 29 | if err := m.coll.FindId(id).One(book); err != nil { 30 | if err == mgo.ErrNotFound { 31 | return nil, errBookNotFound 32 | } 33 | return nil, err 34 | } 35 | 36 | return book, nil 37 | } 38 | -------------------------------------------------------------------------------- /services/book/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | 6 | "github.com/chidiwilliams/go-web-server-tips/errors" 7 | "github.com/chidiwilliams/go-web-server-tips/models" 8 | ) 9 | 10 | var ( 11 | errBookNotFound = errors.NotFound("book") 12 | ) 13 | 14 | type Repository interface { 15 | GetBook(id bson.ObjectId) (*models.Book, error) 16 | CreateBook(book models.Book) error 17 | } 18 | -------------------------------------------------------------------------------- /services/book/service.go: -------------------------------------------------------------------------------- 1 | package book 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | "github.com/chidiwilliams/go-web-server-tips/models" 9 | "github.com/chidiwilliams/go-web-server-tips/services/book/repository" 10 | ) 11 | 12 | type Service interface { 13 | GetBook(id bson.ObjectId) (*models.Book, error) 14 | CreateBook(title string) (*models.Book, error) 15 | } 16 | 17 | func NewService(repository repository.Repository) Service { 18 | return service{repository} 19 | } 20 | 21 | type service struct { 22 | repository repository.Repository 23 | } 24 | 25 | func (s service) CreateBook(title string) (*models.Book, error) { 26 | book := models.Book{ID: bson.NewObjectId(), Title: title, CreatedAt: time.Now().UTC()} 27 | if err := s.repository.CreateBook(book); err != nil { 28 | return nil, err 29 | } 30 | 31 | return &book, nil 32 | } 33 | 34 | func (s service) GetBook(id bson.ObjectId) (*models.Book, error) { 35 | return s.repository.GetBook(id) 36 | } 37 | --------------------------------------------------------------------------------