ในบทความก่อนหน้าใน Series นี้ เราพูดคุยอย่างกว้างขวางเกี่ยวกับ Go net/http Package และการใช้งานสำหรับ Production-ready Web Applications เรา Focus ที่ Routing Aspect และ Quirks/Features ของ http.ServeMux Type
บทความนี้จะปิดท้ายการอภิพาย ServeMux โดย示範 Middleware Functions Implementation กับ Default Router และแนะนำ Standard Library Packages อื่นๆ ที่แน่นอนจะมาใน handy ตอน Develop Web Services ด้วย Go
Middleware ใน Go
Practice การ Setup Shared Functionality ที่ต้อง Run สำหรับ Many/All HTTP Requests เรียกว่า Middleware Operations เช่น Authentication, Logging, Cookie Validation มัก Implement เป็น Middleware Functions ซึ่ง Act On Request แบบ Independent ก่อน/หลัง Regular Route Handlers
Implement Middleware ใน Go ต้อง Ensure Type Satisfies http.Handler Interface ปกติหมายถึง Attach Method Signature ServeHTTP(http.ResponseWriter, *http.Request) ให้ Type ใช้ Method นี้ Type ใดๆ ก็ Satisfy http.Handler Interface ได้
ตัวอย่างง่าย:
package main
import "net/http"
type helloHandler struct {
name string
}
func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello " + h.name))
}
func main() {
mux := http.NewServeMux()
helloJohn := helloHandler{name: "John"}
mux.Handle("/john", helloJohn)
http.ListenAndServe(":8080", mux)
}
Request ส่งไป /john จะ Pass ตรงไป helloHandler.ServeHTTP เริ่ม Server ไป https://localhost:8080/john ดูได้
เพิ่ม ServeHTTP Method ให้ Custom Type ทุกครั้งที่อยาก Implement http.Handler จะ Tedious net/http ให้ http.HandlerFunc Type ใช้ Ordinary Functions เป็น HTTP Handlers ได้
แค่ Ensure Function Signature: func(http.ResponseWriter, *http.Request) แล้ว Convert ให้ http.HandlerFunc Type
package main
import "net/http"
func helloJohnHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello John"))
}
func main() {
mux := http.NewServeMux()
mux.Handle("/john", http.HandlerFunc(helloJohnHandler))
http.ListenAndServe(":8080", mux)
}
แทน mux.Handle ด้วย mux.HandleFunc ส่ง Function ตรงๆ เราใช้ Pattern นี้ Exclusively ในบทความก่อน
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/john", helloJohnHandler)
http.ListenAndServe(":8080", mux)
}
จุดนี้ Name Hardcoded ใน String ต่างจากก่อนที่ Set Name ใน main ก่อนเรียก Handler เพื่อ Remove Limitation ใส่ Handler Logic ใน Closure:
package main
import "net/http"
func helloHandler(name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello " + name))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/john", helloHandler("John"))
http.ListenAndServe(":8080", mux)
}
helloHandler Function เองไม่ Satisfy http.Handler แต่สร้างและ Return Anonymous Function ที่ Satisfy Function นี้ Close Over name Parameter เข้าถึงได้ตอน Called helloHandler Reuse ได้หลาย Names
สิ่งนี้เกี่ยวกับ Middleware อย่างไร? สร้าง Middleware Function ทำแบบเดียวกับด้านบน แทน Pass String ให้ Closure (เหมือน Example) เราสามารถ Pass Next Handler ใน Chain เป็น Argument
Complete Pattern:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Middleware logic goes here...
next.ServeHTTP(w, r)
})
}
middleware Function รับ Handler Return Handler สังเกต Anonymous Function Satisfy http.Handler Interface โดย Cast ให้ http.HandlerFunc Type ท้าย Anonymous Function Transfer Control ให้ next Handler เรียก ServeHTTP() Method ถ้าต้อง Pass Values ระหว่าง Handlers (เช่น Authenticated User ID) ใช้ http.Request.Context() Method Introduced ใน Go 1.7
เขียน Middleware Function 示範 Pattern นี้ Function เพิ่ม Property requestTime ให้ Request Object ใช้โดย helloHandler แสดง Timestamp ของ Request
package main
import (
"context"
"net/http"
"time"
)
func requestTime(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, "requestTime", time.Now().Format(time.RFC3339))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func helloHandler(name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
responseText := "Hello " + name + "
"
if requestTime := r.Context().Value("requestTime"); requestTime != nil {
if str, ok := requestTime.(string); ok {
responseText = responseText + "\nGenerated at: " + str + ""
}
}
w.Write([]byte(responseText))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/john", requestTime(helloHandler("John")))
http.ListenAndServe(":8080", mux)
}

เพราะ Middleware Function Accept/Return http.Handler Type สามารถสร้าง Infinite Chain Middleware Functions Nested ได้
ตัวอย่าง:
mux := http.NewServeMux()
mux.Handle("/", middleware1(middleware2(appHandler)))
ใช้ Library อย่าง Alice Transform ให้ Readable Form:
alice.New(middleware1, middleware2).Then(appHandler)
Templating
แม้ Templates Usage จะลดลงพร้อม Single-page Applications แต่มันยังเป็น Important Aspect ของ Complete Web Development Solution
Go ให้ 2 Packages สำหรับ Templating: text/template และ html/template ทั้งคู่ Same Interface แต่หลังทำ Encoding Behind Scenes Guard Against Code Injection Exploits
Go Templates ไม่ได้ Expressive ที่สุด แต่ทำงานได้ Fine ใช้ Production Applications ได้ จริงๆ Hugo Popular Static Site Generator Base Templating System บนนี้
ดู Quick Look html/template Package ใช้ส่ง HTML Output Response Web Request
สร้าง Template
สร้าง index.html เดียว Directory กับ main.go เพิ่มโค้ด:
{{ range .TodoItems }}
- {{ . }}
{{ end }}
เพิ่มใน main.go:
package main
import (
"html/template"
"log"
"os"
)
func main() {
t, err := template.ParseFiles("index.html")
if err != nil {
log.Fatal(err)
}
todos := []string{"Watch TV", "Do homework", "Play games", "Read"}
err = t.Execute(os.Stdout, todos)
if err != nil {
log.Fatal(err)
}
}
Execute go run main.go Output:
- Watch TV
- Do homework
- Play games
- Read
Congratulations! สร้าง Go Template แรกแล้ว Syntax อธิบายสั้น:
- Go ใช้ Double Braces (
{{และ}}) Delimit Data Evaluation และ Control Structures (Actions) ใน Templates rangeAction Iterate Data Structures เช่น Slices.แทน Current Context ในrangeContext คือ Slicetodosใน Block{{ . }}แทบแต่ละ Element ใน Slice
ใน main.go template.ParseFiles สร้าง Template ใหม่จาก File(s) Template Execute ด้วย template.Execute รับ io.Writer และ Data Apply ให้ Template
Example Execute ไป Standard Output แต่ Execute ไป Destination ใดๆ ได้ ถ้า Satisfy io.Writer Interface เช่น Return Output as Web Request Execute Template ไป ResponseWriter:
package main
import (
"html/template"
"log"
"net/http"
)
func main() {
t, err := template.ParseFiles("index.html")
if err != nil {
log.Fatal(err)
}
todos := []string{"Watch TV", "Do homework", "Play games", "Read"}
http.HandleFunc("/todos", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
err = t.Execute(w, todos)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
http.ListenAndServe(":8080", nil)
}

Section นี้ Quick Intro Go Template Packages Check Documentation text/template และ html/template สำหรับ Complex Use Cases
ถ้าไม่ชอบ Go Templating มี Alternatives เช่น Plush Library
ทำงานกับ JSON
ถ้าต้องทำงาน JSON Objects Standard Library ของ Go มีทุกอย่าง Parse และ Encode JSON ผ่าน encoding/json Package
Default Types
Encode/Decode JSON Object ใน Go Types:
boolJSON Booleansfloat64JSON NumbersstringJSON StringsnilJSON Nullmap[string]interface{}JSON Objects[]interface{}JSON Arrays
Encoding
Encode Data Structure เป็น JSON ใช้ json.Marshal Function ตัวอย่าง:
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
FirstName string
LastName string
Age int
email string
}
func main() {
p := Person{
FirstName: "Abraham",
LastName: "Freeman",
Age: 100,
email: "abraham.freeman@hey.com",
}
json, err := json.Marshal(p)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(json))
}
Program มี Person Struct 4 Fields ใน main สร้าง Instance Person Fields ทั้งหมด Initialized json.Marshal Convert p Structure เป็น JSON Return Byte Slice หรือ Error Handle ก่อน Access JSON Data
Convert Byte Slice เป็น String ใน Go ต้อง Type Conversion ตามตัวอย่าง Run Program Output:
{"FirstName":"Abraham","LastName":"Freeman","Age":100}
Valid JSON Object ใช้ได้ทุกอย่าง สังเกต Default Go ใช้ Property Names เดียวกันใน Struct เป็น Field Names ใน JSON Object แตเปลี่ยนได้ผ่าน Struct Field Tags Struct Field Tags ระบุ JSON Encoder Map รับ Byte Slice JSON Data และ Place Store Decoded Data ถ้า Decode Successful Error Return สมมติ JSON Object: Decode ไป Instance Output: ตัวอย่าง MySQL Driver กับ Under the Hood Driver Registers Itself Available ให้ Access Database สร้าง Close Connection ใช้ Query Table 3 Steps: เรียก Query Return Single Row ใช้ Go มี Good Support NoSQL Databases: Redis, MongoDB, Cassandra ฯลฯ แต่ไม่ Provide Standard Interface ทำงานกับพวกมัน ต้อง Rely Entirely Driver Package สำหรับ Database เฉพาะ ตัวอย่าง:
email Field Left Out เพราะ Not Exported จาก Person
type Person struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
email string `json:"email"`
}
FirstName Property ใน Struct ไป first_name Field ใน JSON Object ฯลฯ เปลี่ยนใน Example ก่อนหน้า Output:
{"first_name":"Abraham","last_name":"Freeman","age":100}
Decoding
json.Unmarshal ใช้ Decode JSON Object ไป Go Struct Signature:
func Unmarshal(data []byte, v interface{}) error
nil
json := `{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", "gender":"male"}`
Person Struct:
func main() {
b := `{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", "gender":"male", "email": "john.smith@hmail.com"}`
var p Person
err := json.Unmarshal([]byte(b), &p)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", p)
}
{FirstName:John LastName:Smith Age:35 email:}
Unmarshal Decode เฉพาะ Fields ที่ Found ใน Destination Type ใน Case นี้ place_of_birth และ gender Ignored เพราะไม่ Map Struct Field ใน Person Behavior นี้ Leverage ได้ Pick Specific Fields จาก Large JSON Object เหมือนกัน Unexported Fields ใน Destination Struct Unaffected แม้มี Corresponding Field ใน JSON Object นั่นคือ email Empty String ใน Output แม้ Present ใน JSON ObjectDatabases
database/sql Package ให้ Generic Interface รอบ SQL (หรือ SQL-like) Databases ต้องใช้กับ Database Driver เช่น Listed ที่นี่ Import Driver ต้อง Prefix Underscore _ Initialize Itdatabase/sql:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
database/sql แต่ไม่ Used Directly ใน Code ช่วย Reduce Dependency Specific Driver Swap Out อื่นได้ Minimal Effortเปิด Database Connection
sql.DB Object:
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
}
sql.Open Prepare Database Abstraction Later Use ไม่ Establish Connection หรือ Validate Connection Parameters ถ้าอยาก Ensure Database Available/Accessible ทันที ใช้ db.Ping():
err = db.Ping()
if err != nil {
log.Fatal(err)
}
ปิด Database Connection
db.Close() ปกติ defer Closing จน Function ที่ Open Connection จบ ปกติ main Function:
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
sql.DB Object Designed Long-lived ไม่ควร Open/Close บ่อย จะเจอ Problems: Poor Reuse/Sharing Connections, Run Out Network Resources, Sporadic Failures Best: Pass sql.DB Around หรือ Make Globally Available Close ตอน Program Done Access DatastoreFetch Data จาก Database
db.Query() Iterate Rows ใช้ rows.Scan() Extract Row ไป Variables ตัวอย่าง:
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
db.QueryRow แทน db.Query Avoid Boilerplate:
var (
id int
name string
)
err = db.QueryRow("select id, name from users where id = ?", 1).Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
fmt.Println(id, name)
NoSQL Databases
Wrap Up
ในบทความนี้ Discuss Essential Aspects Building Web Applications ด้วย Go คุณควรเข้าใจแล้วว่าทำไม Go Programmers หลายคน Swear By Standard Library มัน Comprehensive และ Provide Most Tools Necessary Production-ready Service
ถ้าต้องการ Clarification อะไรเรา Covered Here ส่ง Message มา Twitter ใน Next และ Final Article ใน Series เราจะ Discuss go Tool และใช้ Tackle Common Tasks ในการ Develop ด้วย Go
Thanks for reading, and happy coding!