- สร้าง REST API ด้วย Go: ตั้งแต่เริ่มต้นจนถึง CRUD ด้วย net/http
- 1. ติดตั้ง Go
- 2. สร้างโปรเจกต์
- 3. ทำความเข้าใจ REST API ที่จะสร้าง
- 4. สร้าง Struct สำหรับ Product
- 5. แยก Input Model
- 6. สร้าง In-memory Store
- 7. สร้างคำสั่ง CRUD ใน Store
- 8. สร้าง Helper สำหรับ JSON Response
- 9. อ่าน Path Parameter
- 10. อ่าน JSON Request และทำ Validation
- 11. สร้าง Application
- 12. สร้าง Health Check API
- 13. GET Products
- 14. GET Product ตาม ID
- 15. POST Product
- 16. PUT Product
- 17. DELETE Product
- 18. กำหนด Routing
- 19. Middleware
- 20. Logging Middleware
- 21. Panic Recovery Middleware
- 22. CORS Middleware
- 23. รวม Middleware
- 24. สร้าง HTTP Server
- 25. Source Code ฉบับเต็ม
- 26. รัน API
- 27. ทดสอบ Health Check
- 28. ทดสอบ POST
- 29. ทดสอบ GET ทั้งหมด
- 30. ทดสอบ GET ตาม ID
- 31. ทดสอบ PUT
- 32. ทดสอบ DELETE
- 33. HTTP Status Code ที่ควรรู้
- 34. เพิ่ม Unit Test ด้วย httptest
- 35. โครงสร้างโปรเจกต์เมื่อระบบใหญ่ขึ้น
- 36. เชื่อมต่อ PostgreSQL
- 37. Environment Variable
- 38. Authentication
- 39. API Versioning
- 40. Pagination
- 41. Filtering และ Sorting
- 42. Request ID
- 43. Observability
- 44. Rate Limiting
- 45. Graceful Shutdown
- 46. Dockerize Go API
- 47. Framework จำเป็นหรือไม่?
- 48. Checklist ก่อน Deploy Production
- 49. Architecture ที่แนะนำ
- สรุป
- เอกสารอ้างอิง
#สร้าง REST API ด้วย Go: ตั้งแต่เริ่มต้นจนถึง CRUD ด้วย net/http
Go หรือ Golang เป็นภาษาที่เหมาะกับการพัฒนา Backend และ API เนื่องจากมีจุดเด่นด้านประสิทธิภาพ การใช้ทรัพยากรค่อนข้างต่ำ การทำงานแบบ Concurrent ด้วย Goroutine และมี Standard Library ที่รองรับงาน Web Server อยู่แล้ว
บทความนี้จะสาธิตการสร้าง REST API สำหรับจัดการข้อมูลสินค้า (Product) โดยใช้ net/http ซึ่งเป็น Standard Library ของ Go โดยไม่ต้องติดตั้ง Web Framework เพิ่มเติม
บทความนี้ใช้ Routing รูปแบบ
GET /api/products/{id}ซึ่งรองรับโดยhttp.ServeMuxตั้งแต่ Go 1.22 ขึ้นไป
ณ วันที่เขียนบทความ Go รุ่นเสถียรล่าสุดคือ Go 1.27.1
#สิ่งที่จะได้เรียนรู้
หลังจากจบบทความนี้ เราจะสามารถสร้าง API ที่รองรับ
| Method | Endpoint | หน้าที่ |
|---|---|---|
GET |
/health |
ตรวจสอบสถานะ Server |
GET |
/api/products |
ดึงสินค้าทั้งหมด |
GET |
/api/products/{id} |
ดึงสินค้าตาม ID |
POST |
/api/products |
เพิ่มสินค้า |
PUT |
/api/products/{id} |
แก้ไขสินค้า |
DELETE |
/api/products/{id} |
ลบสินค้า |
นอกจากนี้ยังครอบคลุม
- JSON Request/Response
- HTTP Status Code
- Input Validation
- Path Parameter
- Error Handling
- Middleware
- CORS
- Request Size Limit
- Server Timeout
- การทดสอบด้วย
curl - แนวทางแยกโครงสร้างโปรเจกต์สำหรับ Production
#1. ติดตั้ง Go
ดาวน์โหลด Go ได้จากเว็บไซต์อย่างเป็นทางการ
https://go.dev/dl/
ตรวจสอบเวอร์ชัน
go version
ตัวอย่าง
go version go1.27.1 darwin/arm64
สำหรับบทความนี้แนะนำ Go 1.22 ขึ้นไป เพราะ http.ServeMux รองรับ Method-based Routing และ Path Wildcard แล้ว
#2. สร้างโปรเจกต์
สร้างโฟลเดอร์
mkdir go-rest-api
cd go-rest-api
สร้าง Go Module
go mod init example.com/go-rest-api
จะได้ไฟล์
go.mod
โครงสร้างเริ่มต้น
go-rest-api/
├── go.mod
└── main.go
#3. ทำความเข้าใจ REST API ที่จะสร้าง
เราจะสร้างระบบ Product โดย Product หนึ่งรายการมีข้อมูลดังนี้
{
"id": 1,
"name": "Mechanical Keyboard",
"price": 2490,
"stock": 10,
"created_at": "2026-09-15T00:00:00Z",
"updated_at": "2026-09-15T00:00:00Z"
}
ในตัวอย่างนี้จะเก็บข้อมูลไว้ใน Memory ก่อน เพื่อให้เข้าใจ HTTP API ได้ง่าย โดยยังไม่เชื่อมต่อฐานข้อมูล
#4. สร้าง Struct สำหรับ Product
สร้างไฟล์ main.go
package main
import (
"time"
)
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Tag เช่น
`json:"name"`
ใช้กำหนดชื่อ Field เมื่อแปลง Struct เป็น JSON
#5. แยก Input Model
ไม่ควรรับ Product ทั้ง Struct จาก Client โดยตรง เพราะ Client ไม่ควรกำหนดค่า เช่น
idcreated_atupdated_at
จึงสร้าง Input Model แยกต่างหาก
type ProductInput struct {
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
#6. สร้าง In-memory Store
ตัวอย่างนี้ใช้ map เป็น Data Store
type Store struct {
mu sync.RWMutex
products map[int64]Product
nextID int64
}
func NewStore() *Store {
return &Store{
products: make(map[int64]Product),
nextID: 1,
}
}
ใช้ sync.RWMutex เพื่อป้องกันปัญหา Data Race กรณีมีหลาย Request เข้ามาพร้อมกัน
#7. สร้างคำสั่ง CRUD ใน Store
#ดึงสินค้าทั้งหมด
func (s *Store) List() []Product {
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]Product, 0, len(s.products))
for _, p := range s.products {
items = append(items, p)
}
return items
}
#ดึงสินค้าตาม ID
func (s *Store) Get(id int64) (Product, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.products[id]
return p, ok
}
#เพิ่มสินค้า
func (s *Store) Create(in ProductInput) Product {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UTC()
p := Product{
ID: s.nextID,
Name: strings.TrimSpace(in.Name),
Price: in.Price,
Stock: in.Stock,
CreatedAt: now,
UpdatedAt: now,
}
s.products[p.ID] = p
s.nextID++
return p
}
#แก้ไขสินค้า
func (s *Store) Update(id int64, in ProductInput) (Product, bool) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.products[id]
if !ok {
return Product{}, false
}
p.Name = strings.TrimSpace(in.Name)
p.Price = in.Price
p.Stock = in.Stock
p.UpdatedAt = time.Now().UTC()
s.products[id] = p
return p, true
}
#ลบสินค้า
func (s *Store) Delete(id int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.products[id]; !ok {
return false
}
delete(s.products, id)
return true
}
#8. สร้าง Helper สำหรับ JSON Response
API ส่วนใหญ่สื่อสารข้อมูลด้วย JSON
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}
สร้าง Helper สำหรับ Error
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(
w,
status,
map[string]string{
"error": message,
},
)
}
ตัวอย่าง Error Response
{
"error": "product not found"
}
#9. อ่าน Path Parameter
Route เช่น
/api/products/10
สามารถกำหนด Route
GET /api/products/{id}
แล้วอ่านค่า {id} ด้วย
r.PathValue("id")
สร้าง Helper
func readID(r *http.Request) (int64, error) {
id, err := strconv.ParseInt(
r.PathValue("id"),
10,
64,
)
if err != nil || id < 1 {
return 0, errors.New("invalid product id")
}
return id, nil
}
#10. อ่าน JSON Request และทำ Validation
เราจะจำกัด Request Body ไม่เกิน 1 MB และไม่อนุญาต Field ที่ไม่ได้ประกาศไว้
func decodeInput(
w http.ResponseWriter,
r *http.Request,
) (ProductInput, error) {
var in ProductInput
r.Body = http.MaxBytesReader(
w,
r.Body,
1<<20,
)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&in); err != nil {
return ProductInput{}, err
}
if strings.TrimSpace(in.Name) == "" {
return ProductInput{},
errors.New("name is required")
}
if in.Price < 0 {
return ProductInput{},
errors.New(
"price must be greater than or equal to 0",
)
}
if in.Stock < 0 {
return ProductInput{},
errors.New(
"stock must be greater than or equal to 0",
)
}
return in, nil
}
#11. สร้าง Application
type App struct {
store *Store
}
Handler ต่าง ๆ จะเข้าถึง Data Store ผ่าน App
#12. สร้าง Health Check API
Health Check มีประโยชน์กับ
- Docker
- Kubernetes
- Load Balancer
- Monitoring System
func (a *App) health(
w http.ResponseWriter,
r *http.Request,
) {
writeJSON(
w,
http.StatusOK,
map[string]string{
"status": "ok",
},
)
}
Endpoint
GET /health
Response
{
"status": "ok"
}
#13. GET Products
#ดึงสินค้าทั้งหมด
func (a *App) listProducts(
w http.ResponseWriter,
r *http.Request,
) {
writeJSON(
w,
http.StatusOK,
a.store.List(),
)
}
Route
GET /api/products
#14. GET Product ตาม ID
func (a *App) getProduct(
w http.ResponseWriter,
r *http.Request,
) {
id, err := readID(r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
err.Error(),
)
return
}
product, ok := a.store.Get(id)
if !ok {
writeError(
w,
http.StatusNotFound,
"product not found",
)
return
}
writeJSON(
w,
http.StatusOK,
product,
)
}
Route
GET /api/products/{id}
ตัวอย่าง
GET /api/products/1
#15. POST Product
func (a *App) createProduct(
w http.ResponseWriter,
r *http.Request,
) {
input, err := decodeInput(w, r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
"invalid request: "+err.Error(),
)
return
}
product := a.store.Create(input)
w.Header().Set(
"Location",
fmt.Sprintf(
"/api/products/%d",
product.ID,
),
)
writeJSON(
w,
http.StatusCreated,
product,
)
}
Request
POST /api/products
Content-Type: application/json
Body
{
"name": "Mechanical Keyboard",
"price": 2490,
"stock": 10
}
ควรตอบ HTTP Status
201 Created
#16. PUT Product
func (a *App) updateProduct(
w http.ResponseWriter,
r *http.Request,
) {
id, err := readID(r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
err.Error(),
)
return
}
input, err := decodeInput(w, r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
"invalid request: "+err.Error(),
)
return
}
product, ok := a.store.Update(id, input)
if !ok {
writeError(
w,
http.StatusNotFound,
"product not found",
)
return
}
writeJSON(
w,
http.StatusOK,
product,
)
}
Route
PUT /api/products/{id}
#17. DELETE Product
func (a *App) deleteProduct(
w http.ResponseWriter,
r *http.Request,
) {
id, err := readID(r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
err.Error(),
)
return
}
if !a.store.Delete(id) {
writeError(
w,
http.StatusNotFound,
"product not found",
)
return
}
w.WriteHeader(
http.StatusNoContent,
)
}
ควรตอบ
204 No Content
#18. กำหนด Routing
Go สามารถกำหนด HTTP Method ใน Pattern ได้โดยตรง
func routes(app *App) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc(
"GET /health",
app.health,
)
mux.HandleFunc(
"GET /api/products",
app.listProducts,
)
mux.HandleFunc(
"GET /api/products/{id}",
app.getProduct,
)
mux.HandleFunc(
"POST /api/products",
app.createProduct,
)
mux.HandleFunc(
"PUT /api/products/{id}",
app.updateProduct,
)
mux.HandleFunc(
"DELETE /api/products/{id}",
app.deleteProduct,
)
return mux
}
รูปแบบนี้ทำให้ไม่จำเป็นต้องเขียน
switch r.Method
ในทุก Handler
#19. Middleware
Middleware อยู่ระหว่าง Client และ Handler
Client
|
v
Middleware
|
v
Handler
|
v
Response
ตัวอย่างงานที่เหมาะกับ Middleware
- Logging
- Authentication
- Authorization
- CORS
- Rate Limiting
- Request ID
- Tracing
- Metrics
- Recovery
#20. Logging Middleware
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf(
"%s %s %s",
r.Method,
r.URL.Path,
time.Since(start),
)
},
)
}
#21. Panic Recovery Middleware
func recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
defer func() {
if rec := recover(); rec != nil {
log.Printf(
"panic: %v",
rec,
)
writeError(
w,
http.StatusInternalServerError,
"internal server error",
)
}
}()
next.ServeHTTP(w, r)
},
)
}
#22. CORS Middleware
สำหรับตัวอย่าง Development
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
w.Header().Set(
"Access-Control-Allow-Origin",
"*",
)
w.Header().Set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
)
w.Header().Set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
)
if r.Method == http.MethodOptions {
w.WriteHeader(
http.StatusNoContent,
)
return
}
next.ServeHTTP(w, r)
},
)
}
Production ไม่ควรใช้
Access-Control-Allow-Origin: *โดยไม่พิจารณาความต้องการของระบบ ควรกำหนด Origin ที่อนุญาตอย่างชัดเจน
#23. รวม Middleware
func routes(app *App) http.Handler {
mux := http.NewServeMux()
// routes ...
return recoverer(
logging(
cors(mux),
),
)
}
Flow โดยประมาณ
Request
|
v
Recovery
|
v
Logging
|
v
CORS
|
v
ServeMux
|
v
Handler
#24. สร้าง HTTP Server
ไม่ควรใช้เพียง
http.ListenAndServe(":8080", mux)
สำหรับระบบที่ต้องการควบคุม Timeout ควรสร้าง http.Server
srv := &http.Server{
Addr: ":8080",
Handler: routes(app),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
จากนั้น Start Server
log.Printf(
"API listening on http://localhost%s",
srv.Addr,
)
if err := srv.ListenAndServe(); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
#25. Source Code ฉบับเต็ม
ไฟล์ main.go
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductInput struct {
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
type Store struct {
mu sync.RWMutex
products map[int64]Product
nextID int64
}
func NewStore() *Store {
return &Store{
products: make(map[int64]Product),
nextID: 1,
}
}
func (s *Store) List() []Product {
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]Product, 0, len(s.products))
for _, p := range s.products {
items = append(items, p)
}
return items
}
func (s *Store) Get(id int64) (Product, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.products[id]
return p, ok
}
func (s *Store) Create(in ProductInput) Product {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UTC()
p := Product{
ID: s.nextID,
Name: strings.TrimSpace(in.Name),
Price: in.Price,
Stock: in.Stock,
CreatedAt: now,
UpdatedAt: now,
}
s.products[p.ID] = p
s.nextID++
return p
}
func (s *Store) Update(
id int64,
in ProductInput,
) (Product, bool) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.products[id]
if !ok {
return Product{}, false
}
p.Name = strings.TrimSpace(in.Name)
p.Price = in.Price
p.Stock = in.Stock
p.UpdatedAt = time.Now().UTC()
s.products[id] = p
return p, true
}
func (s *Store) Delete(id int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.products[id]; !ok {
return false
}
delete(s.products, id)
return true
}
type App struct {
store *Store
}
func writeJSON(
w http.ResponseWriter,
status int,
data any,
) {
w.Header().Set(
"Content-Type",
"application/json; charset=utf-8",
)
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}
func writeError(
w http.ResponseWriter,
status int,
message string,
) {
writeJSON(
w,
status,
map[string]string{
"error": message,
},
)
}
func readID(r *http.Request) (int64, error) {
id, err := strconv.ParseInt(
r.PathValue("id"),
10,
64,
)
if err != nil || id < 1 {
return 0, errors.New(
"invalid product id",
)
}
return id, nil
}
func decodeInput(
w http.ResponseWriter,
r *http.Request,
) (ProductInput, error) {
var in ProductInput
r.Body = http.MaxBytesReader(
w,
r.Body,
1<<20,
)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&in); err != nil {
return ProductInput{}, err
}
if strings.TrimSpace(in.Name) == "" {
return ProductInput{},
errors.New("name is required")
}
if in.Price < 0 {
return ProductInput{},
errors.New(
"price must be greater than or equal to 0",
)
}
if in.Stock < 0 {
return ProductInput{},
errors.New(
"stock must be greater than or equal to 0",
)
}
return in, nil
}
func (a *App) health(
w http.ResponseWriter,
r *http.Request,
) {
writeJSON(
w,
http.StatusOK,
map[string]string{
"status": "ok",
},
)
}
func (a *App) listProducts(
w http.ResponseWriter,
r *http.Request,
) {
writeJSON(
w,
http.StatusOK,
a.store.List(),
)
}
func (a *App) getProduct(
w http.ResponseWriter,
r *http.Request,
) {
id, err := readID(r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
err.Error(),
)
return
}
product, ok := a.store.Get(id)
if !ok {
writeError(
w,
http.StatusNotFound,
"product not found",
)
return
}
writeJSON(
w,
http.StatusOK,
product,
)
}
func (a *App) createProduct(
w http.ResponseWriter,
r *http.Request,
) {
input, err := decodeInput(w, r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
"invalid request: "+err.Error(),
)
return
}
product := a.store.Create(input)
w.Header().Set(
"Location",
fmt.Sprintf(
"/api/products/%d",
product.ID,
),
)
writeJSON(
w,
http.StatusCreated,
product,
)
}
func (a *App) updateProduct(
w http.ResponseWriter,
r *http.Request,
) {
id, err := readID(r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
err.Error(),
)
return
}
input, err := decodeInput(w, r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
"invalid request: "+err.Error(),
)
return
}
product, ok := a.store.Update(
id,
input,
)
if !ok {
writeError(
w,
http.StatusNotFound,
"product not found",
)
return
}
writeJSON(
w,
http.StatusOK,
product,
)
}
func (a *App) deleteProduct(
w http.ResponseWriter,
r *http.Request,
) {
id, err := readID(r)
if err != nil {
writeError(
w,
http.StatusBadRequest,
err.Error(),
)
return
}
if !a.store.Delete(id) {
writeError(
w,
http.StatusNotFound,
"product not found",
)
return
}
w.WriteHeader(
http.StatusNoContent,
)
}
func logging(
next http.Handler,
) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf(
"%s %s %s",
r.Method,
r.URL.Path,
time.Since(start),
)
},
)
}
func recoverer(
next http.Handler,
) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
defer func() {
if rec := recover(); rec != nil {
log.Printf(
"panic: %v",
rec,
)
writeError(
w,
http.StatusInternalServerError,
"internal server error",
)
}
}()
next.ServeHTTP(w, r)
},
)
}
func cors(
next http.Handler,
) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
w.Header().Set(
"Access-Control-Allow-Origin",
"*",
)
w.Header().Set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
)
w.Header().Set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
)
if r.Method == http.MethodOptions {
w.WriteHeader(
http.StatusNoContent,
)
return
}
next.ServeHTTP(w, r)
},
)
}
func routes(app *App) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc(
"GET /health",
app.health,
)
mux.HandleFunc(
"GET /api/products",
app.listProducts,
)
mux.HandleFunc(
"GET /api/products/{id}",
app.getProduct,
)
mux.HandleFunc(
"POST /api/products",
app.createProduct,
)
mux.HandleFunc(
"PUT /api/products/{id}",
app.updateProduct,
)
mux.HandleFunc(
"DELETE /api/products/{id}",
app.deleteProduct,
)
return recoverer(
logging(
cors(mux),
),
)
}
func main() {
app := &App{
store: NewStore(),
}
server := &http.Server{
Addr: ":8080",
Handler: routes(app),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Printf(
"API listening on http://localhost%s",
server.Addr,
)
if err := server.ListenAndServe(); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
#26. รัน API
จัดรูปแบบ Source Code
gofmt -w .
ตรวจสอบ Code
go vet ./...
รัน
go run .
จะเห็น
API listening on http://localhost:8080
#27. ทดสอบ Health Check
curl http://localhost:8080/health
Response
{
"status": "ok"
}
#28. ทดสอบ POST
curl \
-X POST \
http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "Mechanical Keyboard",
"price": 2490,
"stock": 10
}'
ตัวอย่าง Response
{
"id": 1,
"name": "Mechanical Keyboard",
"price": 2490,
"stock": 10,
"created_at": "2026-09-15T00:00:00Z",
"updated_at": "2026-09-15T00:00:00Z"
}
#29. ทดสอบ GET ทั้งหมด
curl \
http://localhost:8080/api/products
ตัวอย่าง Response
[
{
"id": 1,
"name": "Mechanical Keyboard",
"price": 2490,
"stock": 10
}
]
#30. ทดสอบ GET ตาม ID
curl \
http://localhost:8080/api/products/1
#31. ทดสอบ PUT
curl \
-X PUT \
http://localhost:8080/api/products/1 \
-H "Content-Type: application/json" \
-d '{
"name": "Mechanical Keyboard Pro",
"price": 3290,
"stock": 20
}'
#32. ทดสอบ DELETE
curl \
-X DELETE \
http://localhost:8080/api/products/1
Status Code ที่ได้
204 No Content
#33. HTTP Status Code ที่ควรรู้
| Code | ความหมาย | ตัวอย่าง |
|---|---|---|
200 OK |
สำเร็จ | GET / PUT |
201 Created |
สร้าง Resource สำเร็จ | POST |
204 No Content |
สำเร็จแต่ไม่มี Body | DELETE |
400 Bad Request |
Request ไม่ถูกต้อง | Validation Error |
401 Unauthorized |
ยังไม่ได้ Authentication | JWT ไม่ถูกต้อง |
403 Forbidden |
ไม่มี Permission | Role ไม่อนุญาต |
404 Not Found |
ไม่พบ Resource | Product ไม่มี |
409 Conflict |
ข้อมูลขัดแย้ง | Duplicate |
422 Unprocessable Content |
Validation เชิงธุรกิจไม่ผ่าน | Business Rule |
429 Too Many Requests |
Request มากเกิน | Rate Limit |
500 Internal Server Error |
Server Error | Unexpected Error |
#34. เพิ่ม Unit Test ด้วย httptest
Go มี Package สำหรับทดสอบ HTTP Handler อยู่แล้ว
net/http/httptest
ตัวอย่าง
func TestHealth(t *testing.T) {
app := &App{
store: NewStore(),
}
req := httptest.NewRequest(
http.MethodGet,
"/health",
nil,
)
rec := httptest.NewRecorder()
routes(app).ServeHTTP(
rec,
req,
)
if rec.Code != http.StatusOK {
t.Fatalf(
"expected status 200, got %d",
rec.Code,
)
}
}
รัน Test
go test ./...
ตรวจ Race Condition
go test -race ./...
#35. โครงสร้างโปรเจกต์เมื่อระบบใหญ่ขึ้น
ไม่ควรเก็บทุกอย่างไว้ใน main.go
ตัวอย่างโครงสร้าง
go-rest-api/
├── cmd/
│ └── api/
│ └── main.go
│
├── internal/
│ ├── handler/
│ │ └── product.go
│ │
│ ├── service/
│ │ └── product.go
│ │
│ ├── repository/
│ │ └── product.go
│ │
│ ├── model/
│ │ └── product.go
│ │
│ └── middleware/
│ ├── logging.go
│ └── auth.go
│
├── migrations/
│
├── go.mod
└── go.sum
Flow
HTTP Request
|
v
Handler
|
v
Service
|
v
Repository
|
v
Database
#Handler
รับผิดชอบ
- HTTP Request
- Path / Query Parameter
- JSON Request
- JSON Response
- HTTP Status Code
#Service
รับผิดชอบ
- Business Logic
- Validation
- Use Case
#Repository
รับผิดชอบ
- Database Query
- Persistence
- Data Access
#36. เชื่อมต่อ PostgreSQL
Production API มักเปลี่ยนจาก In-memory Store ไปเป็นฐานข้อมูล
Go มี Interface มาตรฐาน
database/sql
ตัวอย่าง Driver ที่นิยม เช่น
github.com/jackc/pgx
ตัวอย่าง Connection แนวคิด
db, err := sql.Open(
"pgx",
os.Getenv("DATABASE_URL"),
)
จากนั้นควรตรวจ Connection
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatal(err)
}
อย่า Hard-code Username, Password หรือ Connection String ใน Source Code ควรส่งผ่าน Environment Variable หรือ Secret Manager
#37. Environment Variable
ตัวอย่าง
export PORT=8080
export DATABASE_URL="postgres://user:password@localhost:5432/app"
ใน Go
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
#38. Authentication
API จริงมักต้องเพิ่ม Authentication เช่น
POST /api/auth/login
เมื่อ Login สำเร็จ
{
"access_token": "..."
}
Client ส่ง Token
Authorization: Bearer <token>
จากนั้น Authentication Middleware ตรวจ Token ก่อนให้ Request เข้า Handler
Client
|
v
Auth Middleware
|
+---- Invalid ---> 401
|
v
Handler
#39. API Versioning
เมื่อ API ถูกใช้งานจริงแล้ว การเปลี่ยน Request/Response อาจกระทบ Client
จึงนิยมกำหนด Version
/api/v1/products
เช่น
mux.HandleFunc(
"GET /api/v1/products",
app.listProducts,
)
เมื่อมี Breaking Change
/api/v2/products
#40. Pagination
ไม่ควรส่งข้อมูลหลายหมื่น Record ใน Request เดียว
ตัวอย่าง
GET /api/products?page=1&limit=20
Response
{
"data": [],
"meta": {
"page": 1,
"limit": 20,
"total": 150
}
}
#41. Filtering และ Sorting
ตัวอย่าง Filtering
GET /api/products?min_price=1000
Sorting
GET /api/products?sort=price
Descending
GET /api/products?sort=-price
Search
GET /api/products?q=keyboard
#42. Request ID
สำหรับ Distributed System ควรมี Request ID
X-Request-ID: abc-123
เพื่อค้นหา Log ของ Request เดียวกันได้ง่ายขึ้น
Client
|
v
API Gateway
|
v
Service A
|
v
Service B
ทุก Service ควร Log Request ID เดียวกัน
#43. Observability
Production API ควรมีอย่างน้อย
#Logging
ตัวอย่างข้อมูล
method=POST
path=/api/products
status=201
duration=12ms
request_id=abc123
#Metrics
ตัวอย่าง
http_requests_total
http_request_duration_seconds
http_errors_total
สามารถใช้ร่วมกับ
- Prometheus
- Grafana
#Distributed Tracing
ใช้
- OpenTelemetry
- Jaeger
- Tempo
#44. Rate Limiting
API Public ควรมี Rate Limit
ตัวอย่าง
100 requests / minute / client
เมื่อเกิน Limit
429 Too Many Requests
Rate Limiting สามารถทำได้ที่
- Application
- Reverse Proxy
- API Gateway
- Cloud Load Balancer
#45. Graceful Shutdown
Production Server ควรรอ Request ที่กำลังทำงานให้เสร็จก่อนหยุด Process
แนวคิด
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer stop()
go func() {
<-ctx.Done()
shutdownCtx, cancel :=
context.WithTimeout(
context.Background(),
10*time.Second,
)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
จากนั้นจึง Start Server ตามปกติ
Graceful Shutdown สำคัญเมื่อ Deploy ด้วย
- Docker
- Kubernetes
- Container Platform
#46. Dockerize Go API
ตัวอย่าง Dockerfile
FROM golang:1.27-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 \
GOOS=linux \
go build \
-o /api \
./cmd/api
FROM alpine:3.22
WORKDIR /app
COPY --from=builder /api /api
EXPOSE 8080
CMD ["/api"]
ถ้ายังใช้โปรเจกต์แบบไฟล์ main.go เดียว สามารถเปลี่ยนคำสั่ง Build เป็น
RUN CGO_ENABLED=0 \
GOOS=linux \
go build \
-o /api \
.
#47. Framework จำเป็นหรือไม่?
Go สามารถสร้าง API ด้วย Standard Library ได้โดยไม่ต้องใช้ Framework
อย่างไรก็ตาม เมื่อระบบมี Requirement มากขึ้น อาจพิจารณา Library หรือ Framework เช่น
- Gin
- Echo
- Fiber
- Chi
ตัวอย่างแนวทางเลือก
| แนวทาง | เหมาะกับ |
|---|---|
net/http |
เรียนพื้นฐาน, ลด Dependency, Service ที่ควบคุมเอง |
| Chi | ต้องการ Router แบบเบาและเข้ากับ net/http |
| Gin | ต้องการ Ecosystem และ Middleware จำนวนมาก |
| Echo | ต้องการ Framework ที่มีเครื่องมือพร้อม |
| Fiber | ต้องการ API Style ที่ใช้งานสะดวกและเน้น Performance |
การเข้าใจ net/http ก่อน จะช่วยให้เรียน Framework ต่าง ๆ ได้ง่ายขึ้น เพราะ Framework ส่วนใหญ่ยังทำงานบนแนวคิด HTTP เดียวกัน
#48. Checklist ก่อน Deploy Production
ควรตรวจสอบอย่างน้อย
[ ] Input Validation
[ ] Authentication
[ ] Authorization
[ ] HTTPS
[ ] CORS
[ ] Rate Limiting
[ ] Request Size Limit
[ ] Server Timeout
[ ] Graceful Shutdown
[ ] Structured Logging
[ ] Metrics
[ ] Tracing
[ ] Health Check
[ ] Database Connection Pool
[ ] Migration
[ ] Environment Variables / Secrets
[ ] Unit Test
[ ] Integration Test
[ ] API Documentation
[ ] Vulnerability Scanning
[ ] CI/CD
#49. Architecture ที่แนะนำ
สำหรับ API ขนาดกลางขึ้นไป
Client
|
v
Reverse Proxy
/ API Gateway
|
v
Middleware
|
v
Handler
|
v
Service
|
v
Repository
|
v
PostgreSQL
ส่วน Cross-cutting Concern เช่น
Logging
Metrics
Tracing
Authentication
Rate Limiting
ควรแยกออกจาก Business Logic ให้ชัดเจน
#สรุป
Go สามารถสร้าง REST API ได้อย่างมีประสิทธิภาพโดยใช้ Standard Library เพียงไม่กี่ Package โดยหัวใจสำคัญคือ
net/http
encoding/json
แนวทางพื้นฐานคือ
Client
|
v
HTTP Server
|
v
ServeMux
|
v
Middleware
|
v
Handler
|
v
Service
|
v
Repository
|
v
Database
สำหรับผู้เริ่มต้น แนะนำให้เริ่มจาก net/http ก่อน เพราะจะช่วยให้เข้าใจ
- Request / Response
- HTTP Method
- Status Code
- Routing
- JSON
- Middleware
- Validation
- Error Handling
เมื่อเข้าใจพื้นฐานเหล่านี้แล้ว การต่อยอดไปยัง Gin, Echo, Fiber, Chi, PostgreSQL, Redis, JWT, Docker, Kubernetes หรือ Microservices จะทำได้ง่ายขึ้นมาก
#เอกสารอ้างอิง
-
Go Documentation
https://go.dev/doc/ -
Go Download
https://go.dev/dl/ -
Go Modules Tutorial
https://go.dev/doc/tutorial/create-module -
net/httpPackage
https://pkg.go.dev/net/http -
Go 1.22 Release Notes — Enhanced Routing Patterns
https://go.dev/doc/go1.22 -
Go Release History
https://go.dev/doc/devel/release -
Go Tutorials
https://go.dev/doc/tutorial/