Go 1.7 กับ gorilla/context

เนื่องจากเมื่อคืนผมได้ update version ของ Go จาก 1.6 เป็น 1.7 — compile ผ่าน,​ รันได้ เข้าหน้าเว็บได้ แต่พอเอาขึ้น production server แล้ว panic เมื่อมีการเรียกใช้ gorilla/context — โชคดีที่เข้าไปเปลี่ยน tag ใน docker ทัน และตอนนั้นดึกแล้ว ไม่มีคนใช้งาน

ปัญหาที่ทำให้เกิด panic คือ negroni-sessions return nil กลับมา ทำให้ไม่สามารถดึง user id ออกมาจาก session ได้ ผมเลยเข้าไปดู​ source code ของ negroni-sessions ทำให้รู้ว่าปัญหาจริง ๆ เกิดจาก gorilla/context

gorilla/context ใช้เพื่อเก็บค่าที่เราต้องการใน 1 request
ปกติใน Node.js เราจะใส่ค่านั้นลงไปใน request เลย เช่น

req.isAdmin = true
// ------
const isAdmin = req.isAdmin

แต่เนื่องจาก Go เป็น static type จึงต้องมี Context ใช้เพื่อเก็บค่าแทน และต้องสามารถเรียกได้จากใน goroutine ได้ด้วย เช่น ใน gorilla/context

context.Set(req, "isAdmin", true)
// ------
isAdmin := context.Get(req, "isAdmin").(bool)

แล้วปัญหาอยู่ตรงไหน ?

ลองมาดู code ของ gorilla/context กัน

gorilla/context เก็บ data ไว้ใน map[*http.Request]map[interface{}]interface{}

เมื่อเรียก context.Set จะทำการ lock mutex แล้วเก็บค่าลง map

// Set stores a value for a given key in a given request.
func Set(r *http.Request, key, val interface{}) {
mutex.Lock()
if data[r] == nil {
data[r] = make(map[interface{}]interface{})
datat[r] = time.Now().Unix()
}
data[r][key] = val
mutex.Unlock()
}

แต่!!! Go 1.7 เปลี่ยน http.Request.WithContext เป็น shadow copy ของ Request

// WithContext returns a shallow copy of r with its context changed
// to ctx. The provided ctx must be non-nil.
func (r *Request) WithContext(ctx context.Context) *Request {
if ctx == nil {
panic("nil context")
}
r2 := new(Request)
*r2 = *r
r2.ctx = ctx
return r2
}

หมายความว่า ถ้าเราเรียก context.Set(req, “isAdmin”, true) จาก Middleware และเรียก context.Get(req, “isAdmin”) จาก Handler — ค่า req จะเป็นคนละตัวกัน!!?!?! ทำให้ตอน Get หา req ใน map ไม่เจอ…

วิธีแก้คือ เราต้องใช้ golang.org/x/net/context (ใน 1.7 เปลี่ยนชื่อเป็น context เฉย ๆ) แทน gorilla/context

อย่างแรกที่ต้องทำคือ แก้ code ใน negroni-sessions

จาก

func Sessions(name string, store Store) negroni.HandlerFunc {
return func(res http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Map to the Session interface
s := &session{name, r, store, nil, false}
context.Set(r, sessionKey, s)
// Use before hook to save out the session
rw := res.(negroni.ResponseWriter)
rw.Before(func(negroni.ResponseWriter) {
if s.Written() {
check(s.Session().Save(r, res))
}
})
    // clear the context, we don't need to use
// gorilla context and we don't want memory leaks
defer context.Clear(r)
    next(rw, nr)
}
}

เป็น

func Sessions(name string, store Store) negroni.HandlerFunc {
return func(res http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Map to the Session interface
s := &session{name, r, store, nil, false}
nr := r.WithContext(context.WithValue(r.Context(), sessionKey, s))
// Use before hook to save out the session
rw := res.(negroni.ResponseWriter)
rw.Before(func(negroni.ResponseWriter) {
if s.Written() {
check(s.Session().Save(r, res))
}
})
    next(rw, nr)
}
}

และตอน Get Session จาก

// GetSession returns the session stored in the request context
func GetSession(req *http.Request) Session {
if s, ok := context.Get(sessionKey).(*session); ok {
return s
}
return nil
}

เป็น

// GetSession returns the session stored in the request context
func GetSession(req *http.Request) Session {
if s, ok := req.Context().Value(sessionKey).(*session); ok {
return s
}
return nil
}

แต่เดี๋ยวก่อน!!!

เนื่องจากช่วงนี้มีแต่งาน frontend เลยไม่ได้ตาม framework ฝั่ง backend นาน แต่มี framework ตัวนึงที่เคยกด star ไว้ตอนที่พึ่งเริ่มพัฒนา ตอนนี้กลับไปดูใหม่ปรากฏว่า framework ถูกพัฒนาไปเยอะมาก ๆ จนผมคิดว่าน่าจะเกือบสมบูรณ์ละ ไว้ผมลองเล่นสัก 1–2 projects ก่อน เดี๋ยวจะมาเล่าให้ฟัง

Like what you read? Give acoshift a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.