บันทึกการเรียน Golang วันที่ 8 - ทำ API ง่ายๆ ด้วย gorilla/mux

Go Sep 17, 2023

บันทึกการเรียน Go วันที่ 8 วันนี้เรียนรู้เรื่องการทำ API โดยใช้ตัว gorilla/mux ครับ หลังจากที่อ่านพวกพื้นฐาน ไปบ้างแล้วในเว็บ A Tour of Go (ยังอ่านไม่หมด) ก็เลยคิดว่า ลองทำโปรเจ็คเล็กๆ ขึ้นมา แล้วเรียนไปด้วย น่าจะเห็นภาพมากกว่า วันนี้เลยลองทำ REST API ง่ายๆ ด้วย Go ดู

ติดตั้ง gorilla/mux

go get -u github.com/gorilla/mux

สร้าง Router

r := mux.NewRouter()

การกำหนด path และ handle function สมมติ ต้องการ ให้ route เรามี url แบบนี้/hello

r.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
})

จากโคีดตัวอย่างของ gowebexamples เรายังสามารถกำหนด methods ได้ ว่ารับเฉพาะ GET, POST, PUT, DELETE หรือเปล่า แบบนี้

r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")

สามารถ ดึงค่า params ได้จาก request แบบนี้

r.HandleFunc("books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    vars["title"] // the book title slug
    vars["page"] // the page
})

สมมติ path คือ /books/hello-world/page/100 ค่าที่ได้คือ

  • vars["title"] - จะได้ hello-world
  • vars["page"]- จะได้ค่า 100 (เป็น string)

ลองสร้างโปรเจ็คขึ้นมา

ผมสร้างโปรเจ็คใหม่ขึ้นมา เพื่อลองทดสอบการทำ API โดยจะ response เป็น application/json

  • GET / - เป็น Home ไม่มีอะไร แค่ แสดง Hello World
  • GET /posts - แสดง posts ทั้งหมด
  • GET /posts/{id} - แสดง post by id
  • GET /users - แสดง users ทั้งหมด
  • GET /users/{id} - แสดง user by id
go init go-basic-api

ต่อมาที่ main.go ผมสร้าง function ขึ้นมา กำหนด Router และ path ดังนี้

package main

import (
  "fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

func main() {
	r := mux.NewRouter()

	r.HandleFunc("/", HomeHandler)
	r.HandleFunc("/posts", PostsHandler)
	r.HandleFunc("/posts/{id}", PostHandler)
	r.HandleFunc("/users", UsersHandler)
	r.HandleFunc("/users/{id}", UserHandler)

	log.Fatal(http.ListenAndServe(":8910", r))
}

ซึ่ง handle function ทั้งหมด ยังไม่มีอะไร เพียงแค่ ส่ง text กลับไป

func HomeHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!")
}

func PostsHandler ...
func UsersHandler ...
func PostHandler ...
func UserHandler ...

เมื่อต้องการให้ response เป็น JSON ผมก็เลยต้องกำหนด struct และ ใช้ encoding/json สร้าง struct ง่ายๆ ขึ้นมา 2 ตัวคือ User และ Post พร้อมทั้ง กำหนด default values เป็นค่า mock value มั่วๆ

type User struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Username string `json:"username"`
	Active   bool   `json:"active"`
}

type Post struct {
	ID       string `json:"id"`
	Title    string `json:"title"`
	Body     string `json:"body"`
	AuthorID int    `json:"author_id"`
}

var users = []User{
	{ID: "1", Name: "John Doe", Username: "johndoe", Active: true},
	{ID: "2", Name: "Jane Doe", Username: "janedoe", Active: true},
	{ID: "3", Name: "Michael Jordan", Username: "michaeljordan", Active: true},
	{ID: "4", Name: "John Smith", Username: "johnsmith", Active: true},
}

var posts = []Post{
	{ID: "1", Title: "Hello World", Body: "This is my first post", AuthorID: 1},
	{ID: "2", Title: "Hello World 2", Body: "This is my second post", AuthorID: 1},
	{ID: "3", Title: "Hello World 3", Body: "This is my third post", AuthorID: 2},
	{ID: "4", Title: "Hello World 4", Body: "This is my fourth post", AuthorID: 3},
}

กำหนด Handle function ให้ response เป็น JSON เช่น Posts และ Users ก็ใช้ของ encoding/json ได้เลย แบบนี้

func PostsHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(posts)
}

func UsersHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(users)
}

ส่วน PostHandler และ UserHandler ที่ต้องรับ params id ด้วย ก็จะใช้วิธีนี้

func UserHandler(w http.ResponseWriter, r *http.Request) {
	// 1. get id จาก params
    vars := mux.Vars(r)
	id := vars["id"]

	var user User

    // 2. วน loop หา ค่าใน users ที่ id ตรงกับ params.id
	for _, u := range users {
		if u.ID == id {
			user = u
			break
		}
	}

    // 3. return json user ที่เจอกลับไป
	json.NewEncoder(w).Encode(user)
}

สุดท้ายผมสังเกต มัน return JSON กลับมาแหละ แต่ว่า ตัว content-type จริงๆ เป็น text/plain ไม่ได้เป็น application/json ก็เลย ต้องเปลี่ยน ให้มันเป็น json ด้วย

w.Header().Add("Content-Type", "application/json")

ซึ่งมันต้องมาใส่ ทุกๆ handler function คิดว่า ไม่น่าจะดี เลยคิดว่า มันมีเหมือน node.js middleware หรือเปล่านะ?

ได้คำตอบว่า มี และใช้งานคล้ายๆ กันเลย คือผมใช้เป็นแบบนี้

r := mux.NewRouter()
r.Use(commonMiddleware)

ส่วนตัว commonMiddleware ก็คือ set header content-type

func commonMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Content-Type", "application/json")
		next.ServeHTTP(w, r)
	})
}

สุดท้าย ก็ได้ REST API ด้วย golang ง่ายๆ แล้วครับ ตัว main.go ที่เรียนวันนี้คือ

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

type User struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Username string `json:"username"`
	Active   bool   `json:"active"`
}

type Post struct {
	ID       string `json:"id"`
	Title    string `json:"title"`
	Body     string `json:"body"`
	AuthorID int    `json:"author_id"`
}

var users = []User{
	{ID: "1", Name: "John Doe", Username: "johndoe", Active: true},
	{ID: "2", Name: "Jane Doe", Username: "janedoe", Active: true},
	{ID: "3", Name: "Michael Jordan", Username: "michaeljordan", Active: true},
	{ID: "4", Name: "John Smith", Username: "johnsmith", Active: true},
}

var posts = []Post{
	{ID: "1", Title: "Hello World", Body: "This is my first post", AuthorID: 1},
	{ID: "2", Title: "Hello World 2", Body: "This is my second post", AuthorID: 1},
	{ID: "3", Title: "Hello World 3", Body: "This is my third post", AuthorID: 2},
	{ID: "4", Title: "Hello World 4", Body: "This is my fourth post", AuthorID: 3},
}

func main() {

	r := mux.NewRouter()
	r.Use(commonMiddleware)
	r.HandleFunc("/", HomeHandler)
	r.HandleFunc("/posts", PostsHandler)
	r.HandleFunc("/posts/{id}", PostHandler)
	r.HandleFunc("/users", UsersHandler)
	r.HandleFunc("/users/{id}", UserHandler)

	log.Fatal(http.ListenAndServe(":8910", r))
}

func commonMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Content-Type", "application/json")
		next.ServeHTTP(w, r)
	})
}

func HomeHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!")
}

func PostsHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(posts)
}

func PostHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	var post Post

	for _, p := range posts {
		if p.ID == id {
			post = p
			break
		}
	}
	json.NewEncoder(w).Encode(post)
}

func UsersHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(users)
}

func UserHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	var user User

	for _, u := range users {
		if u.ID == id {
			user = u
			break
		}
	}

	json.NewEncoder(w).Encode(user)
}

FYI: โดยในตัวอย่าง ผมยังไม่ได้ กำหนด http methods นะครับ ฉะนั้น มันก็จะรับทุกๆ http methods ขอเพียงแค่ resource url เดียวกัน เช่น DELETE ก็ได้ response เดียวกันกับ GET

curl -X DELETE http://localhost:8910/users

# ได้ result เดียวกันกับ GET
[{"id":"1","name":"John Doe","username":"johndoe","active":true},{"id":"2","name":"Jane Doe","username":"janedoe","active":true},{"id":"3","name":"Michael Jordan","username":"michaeljordan","active":true},{"id":"4","name":"John Smith","username":"johnsmith","active":true}]
Source Code

Happy Coding ❤️


References

Routing (using gorilla/mux) - Go Web Examples
This example shows how to use the `gorilla/mux` package to create routes with named parameters, GET/POST handlers and domain restrictions.
GitHub - gorilla/mux: Package gorilla/mux is a powerful HTTP router and URL matcher for building Go web servers with 🦍
Package gorilla/mux is a powerful HTTP router and URL matcher for building Go web servers with 🦍 - GitHub - gorilla/mux: Package gorilla/mux is a powerful HTTP router and URL matcher for building G…
Go REST Guide. gorilla/mux Router
The second part of this series demonstrates how to use a router for improved handling of requests when building REST APIs.
Golang Json Marshal Example | Golang Cafe
Golang Json Marshal Example

Tags

Chai Phonbopit

เป็น Web Dev ทำงานมา 10 ปีหน่อยๆ ด้วยภาษา JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจ Web3, Crypto และ Blockchain เขียนบล็อกที่ https://devahoy.com