ข้อผิดพลาดตัวชี้ nil
บทนำ
ในระหว่างกระบวนการเขียนโค้ดครั้งหนึ่ง ฉันจำเป็นต้องเรียกเมธอด Close() เพื่อปิดอ็อบเจ็กต์หลายตัว เหมือนกับโค้ดด้านล่าง
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 ได้ ควรจะทำให้เรียบง่ายกว่านี้ได้ ฉันจึง把它们放入สไลซ์หนึ่งตัว แล้ววนลูปตรวจสอบ
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
}แบบนี้ดูเหมือนจะดีกว่าเล็กน้อย งั้นลองรันดู
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 แน่นอน และสถานการณ์แบบนี้มักเกิดขึ้นกับอินเตอร์เฟซ ดูตัวอย่างง่ายๆ ด้านล่าง
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 เพื่อดูว่ามัน到底是什么
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 ด้วย เพราะประเภทในอินเตอร์เฟซ实际上เป็นตัวชี้
type iface struct {
tab *itab
data unsafe.Pointer
}หากต้องการข้ามประเภทโดยตรง ตรวจสอบว่าค่าเป็น nil หรือไม่ สามารถใช้ reflection ได้ ด้านล่างเป็นตัวอย่าง
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 สามารถใช้ได้ ดูตัวอย่างด้านล่าง
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
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 สามารถเขียนได้ดังนี้
http.ListenAndServe(":8080", nil)เราสามารถส่ง nil Handler เข้าไปโดยตรง แล้วไลบรารี http จะใช้ Handler เริ่มต้นในการจัดการคำขอ HTTP
TIP
ผู้ที่สนใจสามารถดูวิดีโอนี้ Understanding nil - Gopher Conference 2016 อธิบายได้ชัดเจนและเข้าใจง่ายมาก
