Go API ง่ายๆ ไม่ต้องพึ่ง Framework เยอะ
สวัสดีครับทุกคน! วันนี้อยากชวนมาลองเขียน API ด้วยภาษา Go กันดู ผมว่า Go นี่มันเหมาะมากเลยนะสำหรับงานพวก backend, microservice ที่ต้องการความเร็ว แถมเขียนง่าย ไม่ต้องพึ่งเฟรมเวิร์คใหญ่ๆ ก็รันได้ละ (สำหรับงานง่ายๆ นะ)
ปกติผมจะใช้ Go เวลาอยากได้ service เล็กๆ ที่รันไวๆ ไม่ต้องกินแรมเยอะๆ มาดูกันว่าทำยังไง
เริ่มต้นสร้างโปรเจกต์
ก่อนอื่นเลยก็สร้างโปรเจกต์ Go ทั่วไปนี่แหละ
mkdir go-simple-api
cd go-simple-api
go mod init go-simple-api
ทีนี้มาดูโค้ด main.go ง่ายๆ กัน
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv" // เผื่อใช้แปลง string เป็น int
"sync" // สำหรับจัดการเรื่อง concurrency ง่ายๆ
)
// สร้าง struct สำหรับข้อมูลของเรา
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
// นี่คือที่เก็บข้อมูลแบบง่ายๆ ไม่ต้องใช้ DB จริงจัง
// ใช้ sync.Map เพื่อความปลอดภัยเวลามีหลาย request เข้ามาพร้อมกัน
var products = sync.Map{} // map[int]Product
func main() {
// ใส่ข้อมูลตัวอย่างไปซักหน่อย
products.Store(1, Product{ID: 1, Name: "MacBook Air M1", Price: 32900.0})
products.Store(2, Product{ID: 2, Name: "iPad Pro M4", Price: 38900.0})
// กำหนดเส้นทาง (route)
http.HandleFunc("/products", getProducts)
http.HandleFunc("/products/{id}", getProductByID) // Go 1.22 มี Pattern matching ใน HandleFunc แล้วนะ!
http.HandleFunc("/products/add", addProduct)
http.HandleFunc("/products/update/{id}", updateProduct)
http.HandleFunc("/products/delete/{id}", deleteProduct)
fmt.Println("Server กำลังทำงานที่พอร์ต :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Handler สำหรับดึงสินค้าทั้งหมด
func getProducts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// วน loop เก็บสินค้าทั้งหมด
var allProducts []Product
products.Range(func(key, value interface{}) bool {
allProducts = append(allProducts, value.(Product))
return true // ต้อง return true เพื่อให้ Range ทำงานต่อ
})
json.NewEncoder(w).Encode(allProducts)
}
// Handler สำหรับดึงสินค้าตาม ID
func getProductByID(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// ใช้ r.PathValue("id") เพื่อดึงค่าจาก path variable (ตั้งแต่ Go 1.22)
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid product ID", http.StatusBadRequest)
return
}
if p, ok := products.Load(id); ok {
json.NewEncoder(w).Encode(p.(Product))
} else {
http.Error(w, "Product not found", http.StatusNotFound)
}
}
// Handler สำหรับเพิ่มสินค้า
func addProduct(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != "POST" { // เช็คว่าเป็น POST method รึเปล่า
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var newProduct Product
decoder := json.NewDecoder(r.Body)
// สำคัญมาก! ต้องปิด Body หลังจากอ่านเสร็จ ไม่งั้นอาจจะเกิด resource leak
defer r.Body.Close()
if err := decoder.Decode(&newProduct); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
log.Printf("Error decoding product: %v", err) // อันนี้ใส่ไว้ดู error ใน console
return
}
// สร้าง ID ง่ายๆ (ถ้าไม่มี DB ก็ทำแบบนี้แหละ)
newProduct.ID = lenProducts() + 1
products.Store(newProduct.ID, newProduct)
w.WriteHeader(http.StatusCreated) // บอกว่าสร้างสำเร็จ
json.NewEncoder(w).Encode(newProduct)
}
// Handler สำหรับอัพเดทสินค้า
func updateProduct(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != "PUT" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid product ID", http.StatusBadRequest)
return
}
var updatedProduct Product
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
if err := decoder.Decode(&updatedProduct); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
log.Printf("Error decoding product for update: %v", err)
return
}
if _, ok := products.Load(id); ok {
updatedProduct.ID = id // ให้ ID ตรงกับที่ส่งมาใน URL
products.Store(id, updatedProduct)
json.NewEncoder(w).Encode(updatedProduct)
} else {
http.Error(w, "Product not found", http.StatusNotFound)
}
}
// Handler สำหรับลบสินค้า
func deleteProduct(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != "DELETE" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid product ID", http.StatusBadRequest)
return
}
if _, ok := products.Load(id); ok {
products.Delete(id)
w.WriteHeader(http.StatusNoContent) // บอกว่าลบสำเร็จ ไม่มี content ตอบกลับ
} else {
http.Error(w, "Product not found", http.StatusNotFound)
}
}
// ช่วยนับจำนวนสินค้าใน sync.Map
func lenProducts() int {
count := 0
products.Range(func(key, value interface{}) bool {
count++
return true
})
return count
}
การใช้งาน
- ลองยิง API ดู:
- GET Products ทั้งหมด:
GET http://localhost:8080/products(ตอบกลับ:[{"id":1,"name":"MacBook Air M1","price":32900},{"id":2,"name":"iPad Pro M4","price":38900}]) - GET Product ตาม ID:
GET http://localhost:8080/products/1(ตอบกลับ:{"id":1,"name":"MacBook Air M1","price":32900}) - DELETE Product:
DELETE http://localhost:8080/products/delete/3(ตอบกลับ:(Status 204 No Content))
- GET Products ทั้งหมด:
PUT อัพเดท Product: PUT http://localhost:8080/products/update/3 Body (JSON):
{
"name": "Magic Keyboard for iPad",
"price": 7200
}
(ตอบกลับ: {"id":3,"name":"Magic Keyboard for iPad","price":7200})
POST เพิ่ม Product: POST http://localhost:8080/products/add Body (JSON):
{
"name": "Magic Keyboard",
"price": 6900
}
(ตอบกลับ: {"id":3,"name":"Magic Keyboard","price":6900})
รันเซิร์ฟเวอร์:
go run main.go
คุณจะเห็นข้อความ "Server กำลังทำงานที่พอร์ต :8080"
สิ่งที่ได้เรียนรู้และข้อสังเกต
- Standard Library โคตรทรงพลัง: Go ไม่ต้องพึ่ง
gorilla/muxหรือechoก็ยังทำอะไรได้เยอะแยะเลยนะ ยิ่ง Go 1.22 ที่net/httpมันรองรับ Path Matching ในHandleFuncแล้วเนี่ย ยิ่งโคตรง่ายขึ้นไปอีก ไม่ต้องมานั่งเขียน regex เองแล้ว - Error Handling สไตล์ Go: หลายคนอาจจะบ่นว่ามันต้อง
if err != nilเยอะไปหน่อย ผมเองก็เคยบ่นนะ แต่พอชินแล้วจะรู้ว่ามันโคตรชัดเจนเลยว่าอะไรผิดตรงไหน ถ้าเจอerrก็returnไปเลย ง่ายดี defer r.Body.Close(): อันนี้สำคัญมากก ถ้าลืมระวัง memory leak หรือ resource leak ได้นะ! ผมเคยพลาดมาแล้ว หาบั๊กไปเกือบตายกว่าจะเจอ- Concurrency ในตัว:
sync.Mapที่ใช้ในตัวอย่างก็เป็นหนึ่งในวิธีที่ Go จัดการเรื่อง Concurrency ง่ายๆ ทำให้ข้อมูลเราไม่พังเวลามีหลายๆ request เข้ามาพร้อมกัน (อันนี้สำคัญมากสำหรับ production นะ) - เหมาะกับ Microservice: ด้วยความที่มันคอมไพล์เป็น Static Binary ได้ ทำให้ deploy ง่ายมาก ไม่ต้องมี runtime environment พิเศษ แค่เอาไฟล์ไปวางก็รันได้ละ ประหยัดทรัพยากรด้วย
ถ้าใครอยากลองทำ API เล็กๆ หรือ Microservice สักตัว Go นี่เป็นตัวเลือกที่ดีมากๆ เลยนะ ลองเอาไปปรับใช้กันดูครับ!