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
}

การใช้งาน

  1. ลองยิง 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))

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 นี่เป็นตัวเลือกที่ดีมากๆ เลยนะ ลองเอาไปปรับใช้กันดูครับ!

Read more

ไอลีน กู: ตำนานนักสกีฟรีสไตล์ผู้พลิกโฉมวงการและความหมายของชัยชนะ

ไอลีน กู: ตำนานนักสกีฟรีสไตล์ผู้พลิกโฉมวงการและความหมายของชัยชนะ

เจาะลึกเรื่องราวของ Eileen Gu นักสกีฟรีสไตล์ผู้สร้างประวัติศาสตร์ในโอลิมปิก 2026 สถิติที่ไม่เคยมีมาก่อน ประเด็นถกเถียง และความแข็งแกร่งส่วนตัวที่ทำให้เธอก้าวสู่ระดับโลก

By ทีมงาน devdog
วันพระ: คู่มือฉบับสมบูรณ์สำหรับพุทธศาสนิกชนและผู้สนใจยุคใหม่

วันพระ: คู่มือฉบับสมบูรณ์สำหรับพุทธศาสนิกชนและผู้สนใจยุคใหม่

เจาะลึกวันพระและความสำคัญของวันมาฆบูชา 2569 ทั้งวันหยุดราชการ ธนาคาร กิจกรรมเวียนเทียนต้นไม้ และผลกระทบต่อบริการขนส่ง เตรียมตัววางแผนทำบุญและพักผ่อน

By ทีมงาน devdog
ถอดรหัสรักแท้: "บังมัดคลองตันต้นข้าว" เรื่องราวที่สะท้อนการให้อภัยและการเริ่มต้นใหม่

ถอดรหัสรักแท้: "บังมัดคลองตันต้นข้าว" เรื่องราวที่สะท้อนการให้อภัยและการเริ่มต้นใหม่

เจาะลึกงานวิวาห์ "บังมัดคลองตัน" กับ "ต้นข้าว มิสแกรนด์" พร้อมเหตุผลจากใจเจ้าสาวที่เลือกความรักเหนือกาลเวลาและคำวิจารณ์ สู่การเริ่มต้นชีวิตคู่ที่สะท้อนการให้อภัย

By ทีมงาน devdog
ไฮไลท์บอลไทยลีก 2: มหาสารคาม เอสบีที เอฟซี กับฟอร์มร้อนแรงสู่เส้นทางเพลย์ออฟ

ไฮไลท์บอลไทยลีก 2: มหาสารคาม เอสบีที เอฟซี กับฟอร์มร้อนแรงสู่เส้นทางเพลย์ออฟ

เจาะลึกไฮไลท์บอลไทยลีก 2 ของมหาสารคาม เอสบีที เอฟซี กับฟอร์มร้อนแรง ชัยชนะสำคัญจาก ชิตชนก และบทบาทโค้ชดุสิต สู่เส้นทางเพลย์ออฟที่น่าจับตา!

By ทีมงาน devdog