Skip to content

ข้อผิดพลาดตัวชี้ nil

บทนำ

ในระหว่างกระบวนการเขียนโค้ดครั้งหนึ่ง ฉันจำเป็นต้องเรียกเมธอด Close() เพื่อปิดอ็อบเจ็กต์หลายตัว เหมือนกับโค้ดด้านล่าง

go
type A struct {
  b B
  c C
  d D
}

func (a A) Close() error {
  if a.b != nil {
    if err := a.b.Close(); err != nil {
      return err
    }
  }

  if a.c != nil {
    if err := a.c.Close(); err != nil {
      return err
    }
  }

    if a.d != nil {
        if err := a.d.Close(); err != nil {
            return err
        }
    }

  return nil
}

แต่การเขียนคำสั่ง if มากมายขนาดนี้รู้สึกไม่สง่างาม B, C และ D ต่างก็ใช้เมธอด Close ได้ ควรจะทำให้เรียบง่ายกว่านี้ได้ ฉันจึง把它们放入สไลซ์หนึ่งตัว แล้ววนลูปตรวจสอบ

go
func (a A) Close() error {
  closers := []io.Closer{
    a.b,
    a.c,
    a.d,
  }

  for _, closer := range closers {
    if closer != nil {
      if err := closer.Close(); err != nil {
        return err
      }
    }
  }
  return nil
}

แบบนี้ดูเหมือนจะดีกว่าเล็กน้อย งั้นลองรันดู

go
func main() {
  var a A
  if err := a.Close(); err != nil {
    panic(err)
  }
  fmt.Println("success")
}

ผลลัพธ์出乎意料居然พัง了 ข้อมูลข้อผิดพลาดเป็นดังนี้ หมายความว่าไม่สามารถเรียกเมธอดกับตัวรับ nil คำสั่ง if closer != nil ในลูปดูเหมือนไม่ได้ทำหน้าที่กรอง

panic: value method main.B.Close called using nil *B pointer

ตัวอย่างข้างต้นเป็นเวอร์ชันลดรูปของบักที่笔者เคยพบมา ผู้เริ่มต้นหลายคน刚开始อาจทำผิดพลาดแบบนี้เหมือนกัน ต่อไปจะอธิบายว่าเกิดอะไรขึ้น

อินเตอร์เฟซ

ในบทก่อนหน้าเคยกล่าวไว้ว่า nil เป็นค่าศูนย์ของประเภทอ้างอิง เช่น สไลซ์, map, แชนเนล, ฟังก์ชัน, ตัวชี้, อินเตอร์เฟซ สำหรับสไลซ์, map, แชนเนล, ฟังก์ชัน สามารถมองว่าเป็นตัวชี้ทั้งหมด ชี้ไปยังการดำเนินการเฉพาะ

แต่มีเพียงอินเตอร์เฟซเท่านั้นที่แตกต่าง อินเตอร์เฟซประกอบด้วยสองสิ่ง: ประเภทและค่า

เมื่อพยายามกำหนดค่า nil ให้กับตัวแปร จะไม่ผ่านการคอมไพล์ และแสดงข้อความต่อไปนี้

use of untyped nil in assignment

เนื้อหา大致为ไม่สามารถประกาศตัวแปรที่มีค่าเป็น untyped nil ได้ ในเมื่อมี untyped nil ก็ต้องมี typed nil แน่นอน และสถานการณ์แบบนี้มักเกิดขึ้นกับอินเตอร์เฟซ ดูตัวอย่างง่ายๆ ด้านล่าง

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(pa)
  fmt.Println(pa == nil)
}

ผลลัพธ์

<nil>
true
<nil>
false

ผลลัพธ์แปลกมาก ชัดเจนว่าผลลัพธ์ของ pa คือ nil แต่มันไม่เท่ากับ nil เราสามารถใช้ reflection เพื่อดูว่ามัน到底是什么

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.TypeOf(pa))
  fmt.Println(reflect.ValueOf(pa))
}

ผลลัพธ์

<nil>
true
*int
<nil>

จากผลลัพธ์可以看到 มัน实际上是 (*int)(nil) นั่นหมายความว่า pa เก็บประเภทเป็น *int แต่ค่าจริงของมันคือ nil เมื่อทำการเปรียบเทียบค่ากับค่าของประเภทอินเตอร์เฟซ ก่อนอื่นจะตรวจสอบว่าประเภทของพวกมันเท่ากันหรือไม่ หากประเภทไม่เท่ากัน จะตัดสินว่าไม่เท่ากันทันที จากนั้นจึงตรวจสอบว่าค่าเท่ากันหรือไม่ ตรรกะการตัดสินอินเตอร์เฟซส่วนนี้สามารถอ้างอิงจากฟังก์ชัน cmd/compile/internal/walk.walkCompare

ดังนั้น หากต้องการให้อินเตอร์เฟซเท่ากับ nil ต้องให้ค่าของมันเท่ากับ nil และประเภทก็ต้องเท่ากับ nil ด้วย เพราะประเภทในอินเตอร์เฟซ实际上เป็นตัวชี้

go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

หากต้องการข้ามประเภทโดยตรง ตรวจสอบว่าค่าเป็น nil หรือไม่ สามารถใช้ reflection ได้ ด้านล่างเป็นตัวอย่าง

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.ValueOf(pa).IsNil())
}

ผ่าน IsNil() สามารถตรวจสอบว่าค่าเป็น nil ได้โดยตรง เช่นนี้ก็จะไม่เกิดปัญหาข้างต้น ดังนั้นในระหว่างการใช้งานปกติ หากค่าส่งคืนของฟังก์ชันเป็นประเภทอินเตอร์เฟซ หากต้องการส่งคืนค่าศูนย์ ควรส่งคืน nil โดยตรง อย่าส่งคืนค่าศูนย์ของการดำเนินการเฉพาะใดๆ แม้ว่ามันจะใช้อินเตอร์เฟซนี้ได้ แต่ก็จะไม่เท่ากับ nil เลย ซึ่งอาจทำให้เกิดข้อผิดพลาดในตัวอย่าง

สรุป

หลังจากแก้ปัญหาข้างต้นแล้ว ต่อไปดูตัวอย่างด้านล่างนี้

เมื่อตัวรับของโครงสร้างเป็นตัวชี้ตัวรับ nil สามารถใช้ได้ ดูตัวอย่างด้านล่าง

go
type A struct {

}

func (a *A) Do()  {

}

func main() {
  var a *A
  a.Do()
}

โค้ดนี้สามารถทำงานได้ปกติ และจะไม่รายงานข้อผิดพลาดตัวชี้ว่าง

เมื่อสไลซ์เป็น nil สามารถเข้าถึงความยาวและความจุของมัน และสามารถเพิ่มองค์ประกอบ给它ได้

func main() {
  var s []int
  fmt.Println(len(s))
  fmt.Println(cap(s))
  s = append(s, 1)
}

เมื่อ map เป็น nil ยังสามารถเข้าถึงมันได้ แต่ nil map เป็นแบบอ่านอย่างเดียว เมื่อพยายามเขียนจะ引发 panic

go
func main() {
  var s map[string]int
  i, ok := s[""]
  fmt.Println(i, ok)
  fmt.Println(len(s))

  // เมื่อพยายามเขียน จะ引发 panic
  s["a"] = 1 // panic: assignment to entry in nil map

}

คุณสมบัติเกี่ยวกับ nil ในตัวอย่างข้างต้นอาจทำให้สับสนได้ โดยเฉพาะสำหรับผู้เริ่มต้นภาษา Go nil เป็นค่าศูนย์ของประเภทข้างต้นเหล่านี้也就是ค่าเริ่มต้น ค่าเริ่มต้นควรแสดงพฤติกรรมเริ่มต้น และนี่คือสิ่งที่นักออกแบบ Go ต้องการเห็น: ทำให้ nil มีประโยชน์มากขึ้น แทนที่จะรายงานข้อผิดพลาดตัวชี้ว่างโดยตรง แนวคิดนี้同样体现在ไลบรารีมาตรฐาน เช่น การเปิดเซิร์ฟเวอร์ HTTP สามารถเขียนได้ดังนี้

go
http.ListenAndServe(":8080", nil)

เราสามารถส่ง nil Handler เข้าไปโดยตรง แล้วไลบรารี http จะใช้ Handler เริ่มต้นในการจัดการคำขอ HTTP

TIP

ผู้ที่สนใจสามารถดูวิดีโอนี้ Understanding nil - Gopher Conference 2016 อธิบายได้ชัดเจนและเข้าใจง่ายมาก

Golang by www.golangdev.cn edit