1 แนวคิดพื้นฐานของคุณสมบัติ defer
ใน Golang
ในภาษา Go, คำสั่ง defer
ชะลอการดำเนินการของการเรียกใช้ฟังก์ชันถัดไปจนกว่าฟังก์ชันที่มีคำสั่ง defer
จะเสร็จสิ้นการดำเนินการ คุณสามารถคิดว่ามันเหมือนกับบล็อก finally
ในภาษาโปรแกรมมิ่งอื่น ๆ แต่การใช้คำสั่ง defer
มีความยืดหยุ่นและเป็นเอกลักษณ์มากขึ้น
ประโยชน์ของการใช้ defer
คือมันสามารถใช้ในการทำงานที่เกี่ยวกับความสะอาด เช่น ปิดไฟล์ ปลดล็อก mutexes หรือเพียงแค่บันทึกเวลาการออกจากฟังก์ชัน นี่สามารถทำให้โปรแกรมทำงานได้แข็งแรงมากขึ้นและลดปริมาณงานโปรแกรมในการจัดการข้อยกเว้น ในแนวคิดการออกแบบของ Go, การใช้ defer
ถูกแนะนำเพราะมันช่วยให้รักษาโค้ดที่กระชับและอ่านง่ายเมื่อจัดการกับข้อผิดพลาด การทำความสะอาดทรัพยากร และการดำเนินการต่อไปอื่น ๆ
2 หลักการทำงานของ defer
2.1 หลักการทำงานพื้นฐาน
หลักการทำงานพื้นฐานของ defer
คือการใช้ stack (หลักการเข้า-ต่อ-ออกท้าย) เพื่อเก็บฟังก์ชันที่ถูกเลื่อนการดำเนินการไว้ทุกครั้งที่มีการใช้คำสั่ง defer
เมื่อคำสั่ง defer
ปรากฏ ในภาษา Go จะไม่ดำเนินการฟังก์ชันที่ตามหลังทันที แต่จะดันมันเข้า stack ไว้ จะถึงตอนที่ฟังก์ชันที่อยู่ข้างนอกกำลังจะส่งคืน ฟังก์ชันที่ถูก defer จะถูกดำเนินการตามลำดับของ stack โดยฟังก์ชันที่ถูกประกาศในคำสั่ง defer
ที่สุดท้ายจะถูกดำเนินการก่อน
อีกทางที่สำคัญคือ ค่าพารามิเตอร์ในฟังก์ชันที่เป็นผลตามหลังคำสั่ง defer
จะถูกคำนวณและกำหนดเมื่อ defer
ถูกประกาศ ไม่ใช่เมื่อถูกดำเนินการจริงๆ
func example() {
defer fmt.Println("world") // deferred
fmt.Println("hello")
}
func main() {
example()
}
โค้ดข้างต้นจะแสดงผลลัพธ์:
hello
world
world
ถูกพิมพ์ก่อน example
จบการดำเนินการ แม้ว่ามันจะปรากฏก่อน hello
ในโค้ด
2.2 ลำดับการดำเนินการของคำสั่ง defer
หลาย ๆ คำสั่ง
เมื่อฟังก์ชันมีคำสั่ง defer
หลายตัว มันจะถูกดำเนินการตามหลักการเข้า-ต่อ-ออกท้าย ซึ่งมักจะสำคัญสำหรับการเข้าใจตรรกะการทำความสะอาดที่ซับซ้อน ตัวอย่างต่อไปนี้แสดงถึงลำดับการดำเนินการของคำสั่ง defer
หลาย ๆ คำสั่ง:
func multipleDefers() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
fmt.Println("Function body")
}
func main() {
multipleDefers()
}
ผลลัพธ์ของโค้ดนี้จะเป็น:
Function body
Third defer
Second defer
First defer
เนื่องจาก defer
ทำตามหลักการเข้า-ต่อ-ออกท้าย แม้ว่า "First defer" จะเป็นคำสั่ง defer
ที่ปรากฏแรก มันจะถูกดำเนินการสุดท้าย
3 การใช้งานของ defer
ในสถานการณ์ต่าง ๆ
3.1 การปล่อยทรัพยากร
ใน Go language, คำสั่ง defer
มักถูกใช้เพื่อจัดการตรรกะการปล่อยทรัพยากร เช่น การดำเนินการไฟล์และการเชื่อมต่อฐานข้อมูล defer
รับประกันว่าหลังจากการดำเนินการเสร็จ ทรัพยากรที่เกี่ยวข้องจะถูกปล่อยอย่างถูกต้องไม่ว่าจะเกิดเหตุการณ์ใดของการออกจากฟังก์ชัน
ตัวอย่างการดำเนินการไฟล์:
func ReadFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// ใช้ defer เพื่อรับประกันว่าไฟล์จะถูกปิด
defer file.Close()
// ดำเนินการอ่านไฟล์...
}
ในตัวอย่างนี้ เมื่อ os.Open
เปิดไฟล์อย่างเป็นที่ถูกต้อง คำสั่ง defer file.Close()
จะรับประกันว่าทรัพยากรของไฟล์จะถูกปิดอย่างถูกต้องและทรัพยากรของการจัดการไฟล์จะถูกปล่อยเมื่อฟังก์ชันจบการดำเนินการ
ตัวอย่างการเชื่อมต่อฐานข้อมูล:
func QueryDatabase(query string) {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
// รับประกันว่าการเชื่อมต่อฐานข้อมูลจะปิดด้วยการใช้ defer
defer db.Close()
// ดำเนินการค้นหาในฐานข้อมูล...
}
เช่นเดียวกัน defer db.Close()
รับประกันว่าการเชื่อมต่อฐานข้อมูลจะถูกปิดเมื่อออกจากฟังก์ชัน QueryDatabase
ไมว่าจะเป็นการส่งคืนปกติหรือการโยนข้อผิดพลาด
3.2 การทำความสะอาดในการโปรแกรมแบบพร้อมตรง
ในการโปรแกรมแบบพร้อมตรงการใช้ defer
ในการจัดการปลดล็อก mutexes เป็นการปฏิบัติที่ดี มันรับประกันว่าล็อกจะถูกปลดลงอย่างถูกต้องหลังจากการดำเนินการ code ส่วนสำคัญ ทำให้หลีกเลี่ยงการติดล็อค
ตัวอย่างการใช้ Mutex Lock:
var mutex sync.Mutex
func updateSharedResource() {
mutex.Lock()
// ใช้ defer เพื่อให้แน่ใจว่า lock จะถูก release
defer mutex.Unlock()
// แก้ไขข้อมูลใน shared resource...
}
ไมว่าจะเกิดการแก้ไขข้อมูลใน shared resource สำเร็จหรือเกิด panic ในระหว่างทาง, defer
จะรับรองว่า Unlock()
จะถูกเรียกใช้งาน เพื่อให้ goroutines อื่นๆ ที่รอการ lock สามารถรับ lock ได้.
เคล็ดลับ: การอธิบายอย่างละเอียดเกี่ยวกับ mutex locks จะถูกครอบคลุมในบทถัดไป การเข้าใจเหตุการณ์การใช้ defer ในจุดนี้เพียงพอ
3 ข้อผิดพลาดทั่วไปและข้อคิดในการใช้ defer
เมื่อใช้ defer
, แม้ว่าความอ่านง่ายและการรักษาไว้ซึ่งโค้ดจะได้รับการปรับปรุงอย่างมาก แต่ก็ยังมีข้อผิดพลาดและข้อคิดที่ควรจำไว้บางอย่าง
3.1 การประเมินพารามิเตอร์ของฟังก์ชันที่ถูกเลื่อนไว้ทันที
func printValue(v int) {
fmt.Println("Value:", v)
}
func main() {
value := 1
defer printValue(value)
// การปรับค่าของ `value` จะไม่มีผลต่อพารามิเตอร์ที่ถูกส่งไปยัง defer อยู่แล้ว
value = 2
}
// ผลลัพธ์จะเป็น "Value: 1"
แม้ว่าค่าของ value
จะเปลี่ยนไปหลังจากข้อความ defer
, พารามิเตอร์ที่ถ่ายทอดไปยัง printValue
ใน defer
ถูกประเมินและระบุไว้แล้ว จึงทำให้ผลลัพธ์ยังคงเป็น "Value: 1" เหมือนเดิม
3.2 ระมัดระวังเมื่อใช้ defer ภายในวงวน
การใช้ defer
ภายในวงวนอาจเป็นสาเหตุให้ทรัพยากรไม่ถูกปล่อยก่อนที่วงวนจะจบลง ซึ่งอาจทำให้ทรัพยากรรั่วไหลหรือหมดลง
3.3 หลีกเลี่ยง "ปล่อยหลังใช้" ในการเขียนโปรแกรมแบบ concurrent
ในโปรแกรมแบบ concurrent, เมื่อใช้ defer
เพื่อปล่อยทรัพยากร, มันสำคัญที่จะแน่ใจว่า goroutines ทั้งหมดจะไม่พยายามเข้าถึงทรัพยากรหลังจากถูกปล่อย, เพื่อป้องกัน race conditions
4. สังเกตลำดับการทำงานของคำสั่ง defer
คำสั่ง defer
ปฏิบัติตามหลัก "Last-In-First-Out (LIFO)", ที่คำสั่ง defer
ที่ประกาศล่าสุดจะถูกทำงานก่อน
วิธีแก้และมารยาทที่ดี:
- ตลอดเวลามีความระมัดระวังเกี่ยวกับพารามิเตอร์ของฟังก์ชันในคำสั่ง
defer
ที่ถูกประเมินตอนที่ประกาศ - เมื่อใช้
defer
ภายในวงวน, พิจารณาใช้ฟังก์ชันไร้ชื่อหรือเรียกใช้การปล่อยทรัพยากรโดยชัดเจน - ในสภาวะที่มีการใช้งานแบบ concurrent, รับรองว่า goroutines ทั้งหมดได้ทำการดำเนินการเสร็จสิ้นก่อนที่จะใช้
defer
เพื่อปล่อยทรัพยากร - เมื่อเขียนฟังก์ชันที่มีคำสั่ง
defer
หลายๆ รายการ, พิจารณาลำดับการทำงานและตรรกะของมันอย่างรอบคำลงัด
การปฏิบัติตามมารยาทที่ดีเหล่านี้สามารถหลีกเลี่ยงข้อผิดพลาดส่วนใหญ่ที่เกิดขึ้นเมื่อใช้ defer
และทำให้โค้ด Go มีความแข็งแรงและบำรุงรักษาได้มากขึ้น