ความสนุกของ go

ผมชอบภาษาโกมาก เหตุผลก็คือมันสนุก แต่การจะอธิบายว่ามันสนุกตรงไหนนี่ก็ยากมาก
วันนี้เลยอยากจะพยายามอีกสักครั้งที่จะบอกว่ามันสนุกยังไง

ผมมีโจทย์ที่ต้องแก้คือ โปรเจคที่ใช้ go-json-rest มันจะมี struct ของ route หน้าตาแบบนี้

type Route struct {
    HttpMethod string
    PathExp string
    Func HandlerFunc
}

ซึ่งตัว go-json-rest เอง มี middleware ให้ใช้ด้วย แต่ถ้าเราอยากจะระบุแบบเฉพาะเจาะจงไปว่า route ไหน อยากให้ใช้ middleware ตัวไหนเป็นพิเศษ ก็จะยากหน่อยๆ ผมก็เลยสร้าง handler wrapper ขึ้นมาหน้าตาแบบนี้ครับ

type wrapper func(handler rest.HandlerFunc) rest.HandlerFunc

func wrap(h rest.HandlerFunc, wrps ...wrapper) rest.HandlerFunc {
	for _, wrp := range wrps {
		h = wrp(h)
	}
	return h
}

สังเกต type ที่ชื่อ wrapper จะเป็นฟังก์ชัน ที่รับ rest.HandlerFunc แล้วก็คืน rest.HandlerFunc ออกไป เวลาเขียนจริงๆ สมมุติว่าต้องการจะ log ก่อนที่ handler ของจริงจะทำงาน ก็เขียนแบบนี้ครับ

func middleLog(h rest.HandlerFunc) rest.HandlerFunc {
	return func(w rest.ResponseWriter, r *rest.Request) {
		log.Println("logging...")
		h(w, r)
	}
}

 

พอเราได้ wrapper แล้ว ตอนไปสร้าง route ก็เขียนแบบนี้ครับ

route := &rest.Route{HttpMethod: "GET", PathExp: "/hello", Func: wrapper(handler, middleLog)}

แค่นี้ก็สามารถเพิ่ม wrapper ลงไปกี่ตัวก็ได้ เช่น

route := &rest.Route{HttpMethod: "GET", PathExp: "/hello", Func: wrapper(handler, middleLog, middleDebug)}

 

ข้อควรจำคือ ตัวที่ใส่ลงไปก่อน จะถูกทำทีหลัง

ทีนี้ ความสนุกมันจะเกิดขึ้นตอนนี้ เราอยากจะแหย่ ของบางอย่างลงไปใน *rest.Request แบบเนียนๆ ยกตัวอย่างเช่น session ของ database ทีนี้หน้าตาของ wrapper มันจะไม่เหมือนชาวบ้านเขา ตัวอย่างเช่น

func DB(h rest.HandlerFunc, db database.Interface) rest.HandlerFunc {
	return func(w rest.ResponseWriter, r *rest.Request) {
		r.Env["database"] = db
		h(w, r)
	}
}

แบบนี้คือเราอยากจะฝากของบางอย่างไว้ใน *rest.Request.Env แล้วใน handler จริงๆค่อยหยิบไปใช้ ซึ่งก็จะทำให้เทสด้วยการ mock สิ่งนั้นใส่เข้าไปได้ด้วย แต่ว่า wrapper มันจะหน้าตาไม่เหมือนกับ wrapper ด้านบนของเราแล้ว ทีนี้ทางแก้ก็เลยออกมาแบบนี้

func DB(db database.Interface) func(rest.HandlerFunc) rest.HandlerFunc {
	return func(h rest.HandlerFunc) rest.HandlerFunc {
		return func(w rest.ResponseWriter, r *rest.Request) {
			r.Env["database"] = db
			h(w, r)
		}
	}
}

โดยมันจะเพิ่มการ return ขึ้นมาอีกชั้นหนึ่ง แล้วตอนไปใช้งานก็จะออกมาเป็นแบบนี้

db := database.New()
wdb := DB(db)
route := &rest.Route{HttpMethod: "GET", PathExp: "/hello", Func: wrapper(handler, middleLog, wdb)}

ผมสารภาพเลยครับว่า พอถึงบรรทัดนี้ จะมีคนเวียนหัวคล้ายจะเป็นลมละ อย่าตกใจนะครับ ท่านี้ไม่ได้แปลกมาก หากว่าคุณได้เล่น tour มาแล้ว
การที่ db มันถูกนำไปใช้ได้ เป็นเรื่องของ scope ที่ถูกอธิบายไว้ใน tour เลยครับ

ประเด็นของ blog นี้เพื่ออยากจะให้เห็นมุมมองของผมว่า ทำไมผมถึงสนุกกับการเขียนภาษา go นี้มาก ก็เพราะมันมีโจทย์ให้แก้ตลอดนั่นเองครับ