สิ่งที่ต้องระวังเวลาใช้ defer ใน Go

มีหลายบทความที่อธิบายและสอนใช้ defer กันเยอะแล้ว วันนี้เราจะมาดูสิ่งที่ต้องระวังเวลาใช้ defer กันบ้าง

ลองดู code ข้างล่างนี้ว่าถูกหรือผิดยังไง

var db *sql.DB
func queryData() ([]string, error) {
var result []string
    rows, err := db.Query(`select v from a`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var x string
err = rows.Scan(&x)
if err != nil {
return nil, err
}
result = append(result, x)
}
if err = rows.Err(); err != nil {
return nil, err
}
    rows, err = db.Query(`select v from b`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var x string
err = rows.Scan(&x)
if err != nil {
return nil, err
}
result = append(result, x)
}
if err = rows.Err(); err != nil {
return nil, err
}
    return result, nil
}

ถ้าใครรู้ว่าผิดยังไง… ยินดีด้วย คุณไม่ต้องอ่านบทความต่อแล้ว

แต่ถ้ายังไม่รู้ ลองดูตัวอย่างที่ง่ายกว่านี้กัน

type p struct {
v string
}
func (p p) Close() {
fmt.Printf("%s closed\n", p.v)
}
func main() {
x := p{"a"}
defer x.Close()
    x = p{"b"}
defer x.Close()
}

ลองทายว่าถ้ารันแล้วจะได้อะไร ?

ถูกแล้ว !!! คำตอบก็คือ

b closed
a closed

แต่ถ้าเพิ่ม pointer เข้าไปตรงนี้แหละ จะเกิดอะไรขึ้น ?

func (p *p) Close() {
fmt.Printf("%s closed\n", p.v)
}

ลองทายใหม่ว่ารันแล้วได้อะไร ?

b closed
b closed

แล้วทำไมถึงเป็นแบบนี้หล่ะ ?


เวลาที่เราเรียก defer f(x) ตัวแปร x จะถูก copy ทันที รอจนกว่า function ที่เรียกจะจบ f จึงจะถูกเรียก แสดงว่า

func f(a string) {
fmt.Println(a)
}
func main() {
x := "a"
defer f(x)
x = "b"
}

รันแล้วจะได้

a

เพราะตอน defer f(x) คือการเรียก defer f("a")

พอเป็นแบบนี้

func f(a *string) {
fmt.Println(*a)
}
func main() {
x := "a"
defer f(&x)
x = "b"
}

จะได้

b

เพราะว่า

func main() {
x := "a" // 0xc00000e280 "a"
defer f(&x) // f(0xc00000e280)
x = "b" // 0xc00000e280 "b"
}

ตอนเรียก f เราส่งค่า address ของ x เข้าไป พอ x เปลี่ยนค่า ตอนที่เรา print จึงเป็นการเรียกค่าที่ x ชี้อยู่ล่าสุด

ลองกลับไปดูตัวอย่างบนสุดอีกรอบ แล้วเราจะแก้ปัญหานี้ยังไงหล่ะ ?

  1. ใส่ scope ให้มันซะเลย
func queryData() ([]string, error) {
var result []string
    {
rows, err := db.Query(`select v from a`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() { ... }
if err = rows.Err(); err != nil {
return nil, err
}
}
    {
rows, err := db.Query(`select v from b`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() { ... }
if err = rows.Err(); err != nil {
return nil, err
}
}
    return result, nil
}

คราวนี้ rows ก็เป็นคนละตัวกันแล้ว

2. ก็ Close ก่อนใช้อีกรอบสิ

func queryData() ([]string, error) {
var result []string
    rows, err := db.Query(`select v from a`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() { ... }
if err = rows.Err(); err != nil {
return nil, err
}
rows.Close()
    rows, err = db.Query(`select v from b`)
if err != nil {
return nil, err
}
for rows.Next() { ... }
if err = rows.Err(); err != nil {
return nil, err
}
    return result, nil
}

ดังนั้น เวลาเราจะเรียก defer ตัวแปรตัวเดียวกันที่ใช้มากกว่า 1 รอบ ต้องระวังเรื่องนี้กันด้วยนะครับ 😬