- Published on
API Service with Go: Project Structure
- Authors
- Name
- Somprasong Damyos
- @somprasongd
Project Structure
ปัญหาอย่างหนึ่ง เมื่อโปรเจคของเราเริ่มใหญ่ขึ้น มีการเพิ่มโมดูลใหม่ๆ เข้าไปการวางโครงสร้างโปรเจคของเราแบบตอนทำ API Service with Go นั้น ทำให้จัดการโค้ดลำบาก และจะเห็นว่าโค้ดที่เอาไว้จัดการ request แต่ละตัว จะเขียนรวมอยู่ที่เดียวกัน ทั้ง handler, business logic และการเชื่อมต่อฐานข้อมูล ทำให้ยากเขียนทดสอบโค้ด และยากต่อการแก้ไข เช่น ถ้าต้องการเปลี่ยนไปใช้ระบบฐานข้อมูลอื่นอย่าง mongodb จะพบว่าต้องแก้ไขโค้ดเยอะมาก และกระทบกับ business logic ด้วย
งั้นเรามาลองออกแบบโครงสร้างโปรเจคกันใหม่ เพื่อให้สะดวกต่อการแก้ไข รองรับการเพิ่มโมดูล และง่ายต่อการทดสอบ แบบนี้ดู (source code)
├── cmd
│ └── api
│ └── main.go
├── deploy
│ ├── Dockerfile
│ ├── config
│ │ └── pg
│ │ └── sql
│ │ └── init.sql
│ ├── docker-compose.dev.yml
│ ├── docker-compose.prod.yml
│ └── docker-compose.yml
├── pkg
│ ├── app
│ │ ├── app.go
│ │ └── database
│ │ └── gorm.go
│ ├── common
│ │ ├── error.go
│ │ ├── handler-context.go
│ │ ├── logger
│ │ │ └── logger.go
│ │ ├── pagination.go
│ │ ├── response.go
│ │ └── validator.go
│ ├── config
│ │ └── config.go
│ ├── module
│ │ ├── module.go
│ │ ├── m1
│ │ │ ├── module.go
│ │ │ ├── core
│ │ │ │ ├── dto
│ │ │ │ ├── mapper
│ │ │ │ ├── model
│ │ │ │ ├── ports
│ │ │ │ └── service
│ │ │ ├── handler
│ │ │ └── repository
│ │ └── m2
│ │ ├── module.go
│ │ ├── core
│ │ │ ├── dto
│ │ │ ├── mapper
│ │ │ ├── model
│ │ │ ├── ports
│ │ │ └── service
│ │ ├── handler
│ │ └── repository
│ └── util
│ └── some-util.go
├── config.yaml
├── go.mod
├── go.sum
└── Makefile
cmd
โค้ดของ func main()
จะอยู่ที่นี้ โดยโปรเจคนี้จะเป็นการสร้าง api ดังนั้นจะเขียนไว้ที่ cmd/api/main.go
ทำหน้าที่โหลดค่า configuration, สร้าง app.Context, โหลดโมดูลต่างๆ และสั่ง start server
func main() {
// Load config
cfg := config.LoadConfig()
app := app.New(cfg)
// Cleanup when server stopped
defer app.Close()
// For Liveness Probe
app.CreateLivenessFile()
// Initialize data sources
app.InitDS()
// Create router (mux/gin/fiber)
app.InitRouter()
// Initialize module with dependency injection
module.Init(app.Context)
// Start server
app.ServeHTTP()
}
pkg
จะเป็นส่วนโค้ดทั้งหมดของเรา โดยมี
app/app.go
เป็นตัว Initialize สิ่งต่างๆ ที่ต้องใช้งาน แล้วเก็บไว้ในapp.Context
แล้วถึงขั้นตอนการ start servercommon
เป็นโค้ดในส่วนที่ต้องใช้งานร่วมกันในหลายๆ ส่วน เช่น การจัดการ error, logging, การทำ pagination และการตอบ response แบบต่างๆconfig
เป็นโค้ดที่ใช้ในการโหลดค่า configuration (ดูเพิ่มเติม) ) ซึ่งตอนในขณะพัฒนาจะโหลดจากconfig.yaml
แต่ในการ deploy ใช้งานจริงจะโหลดจะ system environmentmodule
เราจะเขียนโมดูลทั้งหมดเอาไว้ที่นี่ โดยmodule/module.go
เป็นตัว Initialize โมดูลต่างๆ และแต่ละโมดูลใช้หลักการของ Hexagonal Architecture ในการเขียน (ดูเพิ่มเติม)
แต่ละโมดูลจะเริ่มที่
module.go
เป็นโค้ดสำหรับจัดการ depencies ทั้งหมด และสร้าง routers ของโมดูลนั้นๆcore
จะเป็นส่วน core หลักของโมดูลนั้น จะประกอบด้วยmodel
โค้ดของ domain model จะอยู่ที่นี้dto
โค้ดที่เกี่ยวกับ dto จะอยู่ที่นี้mapper
เป็นตัว convert ไปมาระหว่าง dto ←→ modelports
เป็นโค้ดส่วน input&output ports ซึ่งก็ คือServiceInterface
และRepositoryInterface
service
โค้ดของ application service หรือ input adapter จะอยู่ที่นี้
handler
โค้ดที่เอาไว้จัดการกับ route handler จะอยู่ที่นี้ โดยจะมี dependency คือ input port โดยจะส่ง application service ของเราเข้าไป และเพื่อที่จะให้สามารถรองรับการเปลี่ยน web framework เราจะใช้วิธีสร้าง handler context ขึ้นมาเอง (ดูเพิ่มเติม)repository
โค้ดในส่วนของ output adapter ที่ application service ต้องเรียกใช้งานutil
เป็นเก็บโค้ดของ utility functions เช่น การแปลงค่าต่างๆ การเข้ารหัส ถอดหรัส password เป็นต้น
deploy
เป็นส่วนของการสร้าง Dockerfile และไฟล์สำหรับการ deploy เช่น docker-compose.yml
Makefile
เนื่องจากคำสั่งหลายๆ คำสั่งนั้นยาวมาก ดังนั้นเราจะเขียนไว้ใน Makefile เพื่อความสะดวกในการรันคำสั่งต่างๆ ตัวอย่างเช่น
SERVICE_NAME=Todo-Api
SERVICE_IMAGE=somprasongd/todo-api
SERVICE_VERSION=1.0.0
export SERVICE_NAME
export SERVICE_IMAGE
export SERVICE_VERSION
dev-up:
@echo "---Start Dev $(SERVICE_NAME) Environtment---"
docker-compose -p todo-api-dev -f ./deploy/docker-compose.yml -f ./deploy/docker-compose.dev.yml up -d
dev-down:
@echo "---Stop Dev $(SERVICE_NAME) Environtment---"
docker-compose -p todo-api-dev -f ./deploy/docker-compose.yml -f ./deploy/docker-compose.dev.yml down
dev:
@echo "---Start Dev $(SERVICE_NAME)---"
go run cmd/api/main.go
d-build:
@echo "---Build $(SERVICE_NAME) $(SERVICE_IMAGE):$(SERVICE_VERSION)---"
docker build -t $(SERVICE_IMAGE):$(SERVICE_VERSION) -f deploy/Dockerfile .
d-build-debug:
@echo "---Build $(SERVICE_NAME) $(SERVICE_IMAGE):$(SERVICE_VERSION)---"
docker build --progress plain -t $(SERVICE_IMAGE):$(SERVICE_VERSION) -f deploy/Dockerfile .
prod-up:
@echo "---Start Prod $(SERVICE_NAME)---"
docker-compose -p task-api-prod -f ./deploy/docker-compose.yml -f ./deploy/docker-compose.prod.yml up -d
prod-down:
@echo "---Stop Prod $(SERVICE_NAME)---"
docker-compose -p task-api-prod -f ./deploy/docker-compose.yml -f ./deploy/docker-compose.prod.yml down
ซึ่งสามารถดูโค้ดแบบเต็มๆ ได้ที่ https://github.com/somprasongd/blog-code/tree/main/golang/goapi-project-structure