延續上一篇的程式碼今天要來CRUD了, 意思是說要搭上 HTTP API 實作功能了, 順便實作一下簡單的三層式架構以及讀入環境變數吧, 可能份量會比較重, 可以多練習幾遍唷!
專案結構會長這樣
- db
- migrations
- query
- sqlc
- internal
- controller
- service
api.go
util.go
main.go
.env
Makefile
sqlc.yaml
.env
DB_DRIVER="postgres"
DB_URL="postgres://postgres:12345@localhost:5435/blog?sslmode=disable"
HOST="localhost:3003"
internal/util.go
package internal
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
DB_DRIVER string
DB_URL string
HOST string
}
func NewConfig() (*Config, error) {
err := godotenv.Load()
if err != nil {
return nil, err
}
return &Config{
DB_DRIVER: os.Getenv("DB_DRIVER"),
DB_URL: os.Getenv("DB_URL"),
HOST: os.Getenv("HOST"),
}, nil
}
插撥個開發熱更新神器 gowatch, 安裝後, 直接在目錄裡 gowatch下去, 存檔就熱更新, 非常好用, 但記得把編譯出來的 binary file 加入 .gitignore唷, 不然會影響 推code到 github的速度!
這一層我們就是 sqlc
internal/service/service.go
package service
import (
db "6/db/sqlc"
"context"
"database/sql"
"fmt"
)
type Service struct {
Q *db.Queries
}
func NewService(q *db.Queries) *Service {
return &Service{Q: q}
}
func (s *Service) CreateUser(user db.CreateUserParams) (*db.User, error) {
resUser, err := s.Q.CreateUser(context.Background(), user)
if err != nil {
return nil, err
}
return &resUser, err
}
func (s *Service) GetUser(id int32) (*db.User, error) {
resUser, err := s.Q.GetUser(context.Background(), id)
if err != nil {
return nil, err
}
return &resUser, nil
}
func (s *Service) DeleteUser(id int32) error {
_, err := s.Q.GetUser(context.Background(), id)
if err != nil {
if err == sql.ErrNoRows {
return fmt.Errorf("user id doesn't exists")
}
return err
}
err = s.Q.DeleteUser(context.Background(), id)
if err != nil {
return err
}
return nil
}
internal/controller/controller.go
package controller
import (
db "6/db/sqlc"
"6/internal/service"
"fmt"
"strconv"
"github.com/gofiber/fiber/v2"
)
type Controller struct {
S service.Service
}
func NewController(q *db.Queries) *Controller {
return &Controller{S: *service.NewService(q)}
}
func (c *Controller) Ping(ctx *fiber.Ctx) error {
return ctx.JSON(fiber.Map{"message": "pong"})
}
func (c *Controller) CreateUser(ctx *fiber.Ctx) error {
userReq := db.CreateUserParams{}
err := ctx.BodyParser(&userReq)
if err != nil {
return ctx.Status(422).SendString("request body error")
}
fmt.Printf("%#+v", userReq)
resUser, err := c.S.CreateUser(userReq)
if err != nil {
return err
}
return ctx.JSON(resUser)
}
func (c *Controller) GetUser(ctx *fiber.Ctx) error {
idStr := ctx.Query("id")
id64, err := strconv.ParseInt(idStr, 10, 32)
if err != nil {
return err
}
id32 := int32(id64)
retUser, err := c.S.GetUser(id32)
if err != nil {
return err
}
return ctx.JSON(retUser)
}
func (c *Controller) DeleteUser(ctx *fiber.Ctx) error {
idStr := ctx.Params("id")
id64, err := strconv.ParseInt(idStr, 10, 32)
if err != nil {
return err
}
id32 := int32(id64)
err = c.S.DeleteUser(id32)
if err != nil {
return ctx.Status(422).JSON(fiber.Map{"error": err.Error()})
}
return ctx.JSON(fiber.Map{"message": "delete user success"})
}
三層式定義好, 來接著做 api.go
api.go
package internal
import (
db "6/db/sqlc"
"6/internal/controller"
"github.com/gofiber/fiber/v2"
)
type API struct {
APP *fiber.App
C controller.Controller
}
func NewAPI(app *fiber.App, q *db.Queries) *API {
a := new(API)
a.C = *controller.NewController(q)
a.APP = app
a.APP.Get("/ping", a.C.Ping)
a.APP.Get("/getuser", a.C.GetUser)
a.APP.Post("/createuser", a.C.CreateUser)
a.APP.Delete("/deleteuser/:id", a.C.DeleteUser)
return a
}
這邊先定義了一個 API 類別, NewAPI constructor, 我們可以看到接收 sqlc 的 queries 當作參數進來, 然後我們pass到 controller, 然後 pass 到 service, 再 pass 到 repository 層, 此為依賴注入, 我們跟資料庫互動, 集中在 repository 層處理!
接著我們定義 function member Ping, 要來做測試用的!
main.go
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
db "6/db/sqlc"
"6/internal"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
_ "github.com/lib/pq"
)
func main() {
cfg, err := internal.NewConfig()
if err != nil {
log.Fatal(err)
}
conn, err := newConn(cfg.DB_DRIVER, cfg.DB_URL)
if err != nil {
log.Fatal(err)
}
app := fiber.New()
app.Use(logger.New())
query := db.New(conn)
api := internal.NewAPI(app, query)
err = api.APP.Listen(cfg.HOST)
if err != nil {
log.Fatal(err)
}
}
// create connection
func newConn(driver string, dsn string) (*sql.DB, error) {
conn, err := sql.Open(driver, dsn)
if err != nil {
return nil, err
}
err = conn.Ping()
if err != nil {
return nil, err
}
fmt.Println("postgres good")
return conn, nil
}
// 封個 printJson 好了
func printJson(v any) {
json, err := json.MarshalIndent(v, "", " ")
if err != nil {
fmt.Println("json marshal failed")
}
fmt.Println(string(json))
}
POST /createuser
curl --location 'http://localhost:3003/createuser' \
--header 'Content-Type: application/json' \
--data-raw '{
"name":"crudtest",
"email":"crudtest@gmail.com",
"password":"12345",
"discount":0.25
}'
GET /getuser
curl --location 'http://localhost:3003/getuser?id=1'
DELETE /deleteuser/:id
curl --location --request DELETE 'http://localhost:3003/deleteuser/1'