#สร้าง 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 ไม่ควรกำหนดค่า เช่น

  • id
  • created_at
  • updated_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 จะทำได้ง่ายขึ้นมาก


#เอกสารอ้างอิง