กระรอกดินต้นฉบับดึงคันโยกที่ปล่อยแปรงทำความสะอาดเมื่อเชือกงานถูกตัด

Go context.AfterFunc: ผูก cleanup กับการยกเลิกโดยไม่ทิ้ง goroutine รอ

พี่โชว์Engineering

งานที่บล็อกรอ I/O หรือทรัพยากรภายนอกมักต้องทำอะไรสักอย่างทันทีเมื่อคำขอถูกยกเลิก เช่น ปลด deadline, ปลุก condition variable หรือคืน lease ถ้าเปิด goroutine อีกตัวไว้รอ ctx.Done() ทุกครั้ง เราเพิ่มทั้ง lifecycle และจุดที่อาจลืมหยุด context.AfterFunc ผูก callback เข้ากับ context โดยตรงและเรียกมันใน goroutine ของตัวเองเมื่อ context ถูกยกเลิกครับ

จุดสำคัญไม่ใช่แค่ “callback ทำงาน” แต่คือผลของฟังก์ชัน stop() ถ้าคืน true แปลว่าเราหยุด callback ก่อนเริ่มได้ ถ้าคืน false callback อาจเริ่มแล้วหรือเคยถูกหยุดไปก่อนหน้า และ stop() ไม่รอให้ callback จบ เราจึงต้องประสาน completion เองเมื่อ cleanup แตะ state ที่ caller จะใช้ต่อ

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	stop := context.AfterFunc(ctx, func() {
		close(done)
	})

	cancel()
	if !stop() {
		<-done // callback เริ่มแล้ว จึงรอให้ cleanup จบ
	}

	quiet := context.AfterFunc(context.Background(), func() {
		panic("must not run")
	})
	if !quiet() {
		panic("expected to stop callback before it ran")
	}
	fmt.Println("cleanup completed and unused callback stopped")
}

อย่าใช้ callback ทำงานหนักโดยไม่มีขอบเขต เพราะมันรันแยกและไม่ได้รับการรอโดยอัตโนมัติ ทำให้ shutdown เสร็จก่อน cleanup ได้ อีกจุดคือถ้า context ถูกยกเลิกก่อนเรียก AfterFunc callback จะเริ่มทันทีใน goroutine ใหม่ ดังนั้น callback ต้องทนต่อการถูกเรียกเร็วและควร idempotent เมื่อไปแตะทรัพยากรร่วม

Recap

  • ใช้ AfterFunc เมื่อ cleanup ผูกกับ cancellation จริง
  • ตรวจค่าจาก stop() และรอ callback เองเมื่อจำเป็น
  • ทำ callback ให้สั้นและทนต่อการเรียกทันที
  • อย่าเดาว่า stop() รอ callback ให้จบ