Go 1.27 เพิ่ม goroutine leak profiler ที่ชี้ว่า goroutine รั่วอยู่บรรทัดไหนขณะระบบทำงานจริง
Go 1.27 เพิ่ม goroutine leak profiler ที่ชี้ได้ว่า goroutine ค้างรออยู่ที่บรรทัดไหน รูปแบบการรั่วที่เจอบ่อยคือฝั่งส่งกับฝั่งรับไม่ตรงกัน

Go 1.27 เพิ่มเครื่องมือวิเคราะห์ตัวใหม่อย่าง goroutine leak profiler เข้ามาช่วยตรวจหาปัญหา goroutine ค้างสะสมในระบบ ซึ่งเป็นเรื่องที่คนเขียน Go เจอกันบ่อย
บทความบน The Go Blog อธิบายอาการนี้ว่า goroutine ตัวหนึ่งค้างรอทำงานต่อ แต่เงื่อนไขที่จะทำให้ goroutine ตัวนั้นกลับมาทำงานไม่มีทางเกิดขึ้นอีกแล้ว
goroutine ที่รั่วจะค้างสะสมเพิ่มขึ้นเรื่อยๆ ส่งผลให้โปรแกรมกินหน่วยความจำมากขึ้น ทั้งจาก stack ของตัว goroutine เอง และจากข้อมูลต่างๆ ในหน่วยความจำที่ goroutine นั้นยังอ้างอิงค้างไว้ ทำให้ Garbage Collector (GC) ไม่สามารถคืนหน่วยความจำส่วนนี้ได้ แถม GC ยังต้องเสียรอบซีพียูไปสแกนหน่วยความจำเหล่านี้ซ้ำๆ ทุกรอบ ยิ่งถ้าโปรแกรมตั้งค่า GOMEMLIMIT ไว้ ก็จะยิ่งเห็นผลกระทบชัดเจนขึ้น
ถ้าใครดูแลระบบ Go บน production แล้วเคยเจอปัญหาหน่วยความจำค่อยๆ ไต่ระดับสูงขึ้นเรื่อยๆ โดยหาต้นเหตุไม่เจอ อาการ goroutine leak มักเป็นสาเหตุแรกๆ ที่คนนึกถึง แต่ก่อนหน้านี้เรายังไม่มีเครื่องมือที่ชี้เป้าได้ตรงๆ ว่า goroutine ตัวไหนรั่ว และค้างอยู่ที่โค้ดบรรทัดไหน
เครื่องมือเดิมมีอยู่แล้ว แต่ยังตรวจจับบน Production ได้ยาก

ในฝั่งการเขียนเทสต์ เรามีเครื่องมือช่วยตรวจจับ goroutine leak อยู่แล้ว 2 ตัวหลักๆ:
- goleak: ไลบรารีโอเพนซอร์สที่ช่วยตรวจเช็กว่าหลังรันเทสต์แต่ละตัวเสร็จ ยังมี goroutine ไหนรันค้างและไม่ยอมจบการทำงานหรือไม่
- synctest: แพ็กเกจที่เพิ่มเข้ามาใน standard library ตั้งแต่ Go 1.25 ช่วยให้เราควบคุมจังหวะและลำดับการทำงานแบบ concurrent ได้ ทำให้เขียนเคสทดสอบที่จำลองยากๆ ให้รันซ้ำได้อย่างแม่นยำ
แต่ข้อจำกัดสำคัญคือ เครื่องมือทั้งสองตัวนี้ใช้ได้เฉพาะตอนรันเทสต์ ไม่ได้ตรวจจับระบบที่กำลังรันอยู่จริงบน production ซึ่งมักมีปริมาณการใช้งานและพฤติกรรมซับซ้อนเกินกว่าที่การทดสอบจะจำลองได้ครบ
ส่วนเครื่องมือบน production เดิมอย่าง goroutine profile ทั่วไป ก็เป็นเพียงการรายงานสถานะคร่าวๆ ว่าตอนนั้นมี goroutine ค้างอยู่ที่จุดไหนบ้าง จุดอ่อนสำคัญคือมันแยกไม่ออกระหว่าง goroutine ที่รั่วจริง กับ goroutine ที่กำลังรอทำงานตามปกติ เช่น ในช่วงที่มี request พุ่งเข้ามาเยอะๆ สุดท้ายนักพัฒนาจึงต้องมานั่งเดาและตีความตัวเลขเอง ทำให้ goroutine leak เล็กๆ น้อยๆ หลุดรอดสายตาไปได้เป็นเดือนเป็นปีโดยไม่มีใครรู้
Goroutine leak profiler แม่นยำขึ้น แต่จำกัดขอบเขตชัดเจน
สิ่งที่ Go 1.27 เพิ่มเข้ามาคือ profile ที่คัดกรองเฉพาะ goroutine ที่รั่วจริงๆ จุดเด่นที่สุดคือความแม่นยำสูง แทบไม่มี false positive หรือเข้าใจผิดว่า goroutine ปกติกำลังรั่วเลย เราจึงไม่ต้องมานั่งเสียเวลาตีความตัวเลขเหมือนแต่ก่อน แถมฟีเจอร์นี้ยังเบามากพอที่จะเปิดใช้งานบนระบบ production ได้จริง
แต่ฟีเจอร์นี้ก็แลกมากับการจำกัดขอบเขตการตรวจจับ โดยจะตรวจจับเฉพาะ goroutine ที่ติดค้างถาวรอยู่กับ 2 สิ่งนี้เท่านั้น:
- Channel: ท่อส่งข้อมูลระหว่าง goroutine
- Synchronization primitives ในแพ็กเกจ sync: กลไกจัดการการทำงานร่วมกัน เช่น Mutex และ WaitGroup
แม้จะดูเหมือนจำกัดขอบเขต แต่ตัวบทความก็ระบุไว้ชัดเจนว่า แค่สองกลุ่มนี้ก็ครอบคลุมรูปแบบการรั่วส่วนใหญ่แล้ว
กรณีที่พบบ่อย: จำนวนครั้งที่ฝั่งส่งกับฝั่งรับไม่ตรงกัน
ตัวอย่างคลาสสิกที่บทความของ Go ยกมาคือฟังก์ชันกระจายงานแบบพื้นฐาน ซึ่งสร้าง goroutine หลายตัวมาช่วยกันประมวลผล แล้วส่งผลลัพธ์กลับมารวมที่ channel เดียว:
func processWorkItems(ws []workItem) ([]workResult, error) {
ch := make(chan result)
for _, w := range ws {
go func() {
res, err := processWorkItem(w)
ch <- result{res, err}
}()
}
var results []workResult
for range len(ws) {
r := <-ch
if r.err != nil {
return nil, r.err
}
results = append(results, r.res)
}
return results, nil
}จุดตายของโค้ดนี้อยู่ที่บรรทัด ch := make(chan result) ซึ่งเป็นการสร้าง unbuffered channel ทำให้ goroutine ฝั่งส่งต้องบล็อกรอจนกว่าจะมีฝั่งรับมารับข้อมูลไปเสมอ ตราบใดที่ทุกงานสำเร็จและลูปฝั่งรับทำงานจนครบ ทุกอย่างก็จะดูปกติดี
แต่ปัญหาจะเกิดขึ้นเมื่อมีงานใดงานหนึ่งเกิด error (r.err != nil) เพราะฟังก์ชันจะ return ออกไปทันที ทำให้ลูปรับข้อมูลหยุดทำงาน ส่งผลให้ goroutine ตัวที่เหลือที่กำลังจะส่งผลลัพธ์เข้า ch ต้องบล็อกค้างอยู่อย่างนั้นตลอดไปโดยไม่มีใครมารับอีกเลย ข้อผิดพลาดลักษณะนี้พบได้บ่อยมากในระบบจริง แม้กระทั่งในระบบ production ของ Uber
เมื่อเราดึง goroutine leak profile ออกมาดู เครื่องมือจะชี้บรรทัดที่ใช้ส่งข้อมูลเข้า channel ได้ทันที:
(pprof) list processWorkItems
Total: 116
ROUTINE ======================== main.processWorkItems.func1 in .../main.go
0 116 (flat, cum) 100% of Total
. . 31: go func() {
. . 32: res, err := processWorkItem(w)
. 116 33: ch <- result{res, err}
. . 34: }()
ตัวเลข 116 บอกว่า ตอนที่เก็บ profile มี goroutine 116 ตัวค้างรอส่งข้อมูลอยู่ที่บรรทัด 33 และทุกครั้งที่ processWorkItems เจอ error แล้ว return ออกไปทั้งที่ยังมี goroutine ตัวอื่นรอส่งเข้า ch อยู่ ตัวเลขนี้ก็จะขยับสูงขึ้นไปอีก นี่คือสัญญาณชัดเจนของ goroutine leak
วิธีแก้ปัญหานี้แบบง่ายๆ ตามที่บทความแนะนำ คือการกำหนดขนาด buffer ของ channel ให้เท่ากับจำนวนงานทั้งหมด:
ch := make(chan result, len(ws))เมื่อเปลี่ยนมาใช้ buffered channel ตัว goroutine ฝั่งส่งจะส่งข้อมูลเข้า buffer แล้วจบการทำงานได้ทันทีโดยไม่ติดบล็อก ต่อให้ฟังก์ชันหลักเจอ error แล้ว return ออกไปก่อน goroutine เหล่านั้นก็จะไม่ค้างอีกต่อไป
ถ้าใช้ net/http/pprof อยู่แล้ว แทบไม่ต้องเขียนโค้ดเพิ่ม
เราสามารถเรียกใช้งาน profiler ตัวใหม่นี้ได้ 2 ช่องทาง:
- เรียกผ่านโค้ดด้วยแพ็กเกจ runtime/pprof โดยระบุชื่อ profile ว่า
goroutineleak - เรียกผ่าน HTTP handler ของแพ็กเกจ net/http/pprof
ช่องทางที่สองคือทางลัดสำหรับระบบที่ตั้งค่าไว้ก่อนแล้ว ถ้าเซอร์วิสของคุณติดตั้ง net/http/pprof ไว้อยู่แล้ว ก็แทบไม่ต้องเขียนโค้ดเพิ่มเลยแม้แต่บรรทัดเดียว เมื่ออัปเกรดเป็น Go 1.27 ตัว profile นี้จะพร้อมใช้งานทันทีที่ endpoint /debug/pprof/goroutineleak บน host และ port ที่เราเปิดไว้
ตรวจสอบผลลัพธ์ได้ 2 วิธีเช่นกัน:
- ดูผ่านเว็บเบราว์เซอร์: เปิดเข้าไปดูหน้าเว็บของ pprof ได้โดยตรง
- วิเคราะห์ผ่าน CLI: ดาวน์โหลดข้อมูลมาเป็นไฟล์แล้วเปิดด้วย
go tool pprofซึ่งเป็นเครื่องมือมาตรฐานที่แถมมากับ Go อยู่แล้ว (ตัวอย่างพอร์ตlocalhost:6060เป็นเพียงค่าสมมติในการทดสอบ ให้เปลี่ยนเป็น host และ port ของระบบจริง)
$ curl http://localhost:6060/debug/pprof/goroutineleak > leak.prof
$ go tool pprof leak.profเมื่อเปิดเข้า pprof แล้ว เราสามารถใช้คำสั่ง list ตามด้วยชื่อฟังก์ชันที่ต้องการตรวจสอบได้เลย:
(pprof) list processWorkItems
ผลลัพธ์จะแสดงบรรทัดโค้ดที่ goroutine ค้างอยู่ พร้อมตัวเลขจำนวน goroutine ที่ติดค้าง ณ จุดนั้นทันที นอกจากนี้ หากใครอยากทดลองเล่นด้วยตัวเอง บทความยังแนบ ตัวอย่าง leak หลายรูปแบบบน Go Playground ให้เข้าไปกดรันดูได้เลย
แบ่งกรณี goroutine leak เป็น 3 กลุ่มให้จำง่าย
บทความยกตัวอย่างการรั่วที่เกิดในโค้ดจริงไว้หลายแบบ เรียงจากง่ายไปซับซ้อน ถ้าจัดกลุ่มให้จำง่ายก็ได้ประมาณนี้:
-
จำนวนครั้งที่ส่งและรับข้อมูลผ่าน channel ไม่ตรงกัน: เป็นกรณีเดียวกับตัวอย่างข้างต้น ซึ่งเกิดได้หลายลักษณะ เช่น
- ส่งข้อมูลซ้ำสองรอบเพราะลืมใส่
returnหลังจากส่ง error ไปแล้ว แต่ฝั่งรับรอรับแค่ครั้งเดียว ฝั่งส่งจึงค้างอยู่ที่การส่งรอบสองตลอดไป - ฝั่งรับ
returnออกจากฟังก์ชันไปก่อน ทำให้ฝั่งส่งที่ส่งข้อมูลตามมาทีหลังไม่มีคนรับ - ใช้คำสั่ง
selectเพื่อรอรับข้อมูลหลายช่องทาง แต่ context หมดเวลาก่อน ทำให้ worker ที่ส่งข้อมูลมาทีหลังไม่มีคนรับข้อมูล ทั้งสองกรณีหลังมักแก้ได้ง่ายๆ ด้วยการกำหนด buffer ของ channel ไว้อย่างน้อย 1 ช่อง
- ส่งข้อมูลซ้ำสองรอบเพราะลืมใส่
-
วนอ่านข้อมูลจาก channel ด้วย
rangeแต่ไม่ได้close: ลูปfor rangeบน channel จะหยุดทำงานก็ต่อเมื่อสั่งclosechannel นั้นเท่านั้น ถ้าฝั่งส่งกระจายงานจนครบแต่ลืมปิด channel ฝั่ง worker ทุกตัวที่วนลูปรออยู่จะติดค้างถาวร ซึ่งการลืมcloseเป็นข้อผิดพลาดที่พบบ่อยมาก นอกจากนี้ ในตัวอย่างโค้ดยังมีอีกกรณีคือ ถ้ากำหนดจำนวน worker เป็น 0 ฝั่งส่งงานก็จะบล็อกค้างเสียเอง เพราะไม่มี worker มารับงานไปทำเลยสักตัว -
ปัญหาเกี่ยวกับ Lock และ WaitGroup:
sync.Mutex: กรณีคลาสสิกคือ สั่งล็อกไว้ที่ต้นลูป แต่มีเงื่อนไขbreakออกจากลูปในบางกรณีโดยลืมปลดล็อกก่อน ทำให้รอบถัดไปหรือ goroutine อื่นที่มาขอล็อกตัวเดิมต้องติดค้างถาวรsync.WaitGroup: เรียกwg.Wait()ผิดจังหวะ เช่น เรียกข้างในลูปที่กำลังสร้าง goroutine แทนที่จะเรียกหลังจบลูป ทำให้ลูปต้องรอ goroutine ก่อนหน้าจบ ทั้งที่ยังไม่ได้สร้าง goroutine ตัวที่เหลือขึ้นมาทำงานด้วยซ้ำ
เคสจริงจากโปรเจกต์โอเพนซอร์สชั้นนำ
ปัญหา goroutine leak ไม่ได้เกิดขึ้นแค่ในตัวอย่างสอนเขียนโค้ด แต่ยังเคยเกิดขึ้นจริงใน codebase ระดับอุตสาหกรรมและโปรเจกต์โอเพนซอร์ส โดยในบทความได้ยกตัวอย่าง Pull Request (PR) ที่แก้ปัญหาเหล่านี้ไว้ด้วย:
- CockroachDB: เกิดจากการลืมปลดล็อกก่อนสั่ง
breakออกจากลูป ส่งผลให้เมื่อเรียกใช้เมธอดเดิมในรอบถัดไป ฟังก์ชันจะค้างทันทีเพราะไม่สามารถขอล็อกที่ยังค้างอยู่ได้ - etcd: เกิดจากลำดับการทำงานของ channel ไม่เป็นไปตามที่คิดไว้ โดย goroutine ฝั่ง
runกับฝั่งStopจับคู่กันแล้วจบไปก่อน ฝั่งStatusจึงค้างตั้งแต่ตอนส่งคำขอเข้า channel เพราะไม่เหลือใครมารับคำขอนั้นอีกแล้ว - Kubernetes และ Moby: เกิดปัญหา Deadlock จากการใช้ channel ร่วมกับ mutex โดยฝั่งหนึ่งถือ lock ไว้แล้วค้างรอส่งข้อมูลเข้า channel ที่ไม่มีใครมารับ ส่วนอีกฝั่งก็ไปติดที่ lock ตัวเดียวกันนั้นก่อนจะย้อนกลับมารับข้อมูลได้ ส่งผลให้ทั้งสองฝั่งบล็อกการทำงานของกันและกัน
ถ้าท่ารั่วพวกนี้ยังเล็ดลอดไปอยู่ใน codebase ระดับนั้นได้ มันก็เล็ดลอดเข้ามาในโค้ดของเราได้เหมือนกัน
เบื้องหลังการทำงาน: ใช้กลไก GC ตรวจหา goroutine ที่ยังมีชีวิตอยู่

หัวใจหลักของความแม่นยำในฟีเจอร์นี้ อยู่ที่นิยาม liveness ว่า goroutine ตัวไหนที่ถือว่ายังมีชีวิตอยู่
goroutine ยังมีชีวิตอยู่ เมื่อเข้าเงื่อนไขข้อใดข้อหนึ่งต่อไปนี้:
- ไม่ได้ติดค้างที่ primitive ใดเลย ไม่ว่าจะเป็น channel หรือ lock
- แม้จะติดค้างอยู่กับ primitive บางตัว แต่ยังมี goroutine อื่นที่มีชีวิตอยู่อ้างอิง primitive ตัวนั้น
สาเหตุที่ต้องมีเงื่อนไขข้อ 2 เพราะตราบใดที่ยังมี goroutine ที่ทำงานอยู่อ้างอิง primitive ตัวนั้น ก็ยังมีโอกาสที่จะส่งข้อมูลหรือปลดล็อกให้ goroutine ที่ค้างอยู่กลับมาทำงานต่อได้
แทนที่จะต้องสร้างระบบตรวจจับขึ้นมาใหม่ทั้งหมด Go ได้นำกลไกของ Garbage Collector (GC) ที่มีอยู่เดิมมาประยุกต์ใช้ เพราะ GC สามารถไล่สายการอ้างอิงในหน่วยความจำได้อยู่แล้ว
ปกติแล้ว GC จะใช้ goroutine ทุกตัวเป็น root ในการสแกนหาข้อมูลในหน่วยความจำ แต่ในรอบตรวจจับ leak กลไกจะเปลี่ยนไป โดยเริ่มสแกนเฉพาะ goroutine ที่ไม่ได้ติดบล็อกเท่านั้น แล้วไล่ตาม pointer ไปเรื่อยๆ ถ้าพบว่า goroutine ที่ทำงานอยู่เชื่อมโยงไปยัง primitive ตัวใด ทั้ง primitive ตัวนั้นและ goroutine ที่ค้างรออยู่ ก็นับว่า "ยังมีชีวิตอยู่" ด้วย ระบบจะวนลูปสแกนซ้ำแบบนี้ไปเรื่อยๆ จนไม่มี goroutine ใหม่เพิ่มเข้ามา และ goroutine ที่ยังติดค้างอยู่หลังสแกนเสร็จสิ้น ก็คือ goroutine ที่รั่วจริงๆ
แนวคิดนี้มาจากงานวิจัยร่วมระหว่าง Aarhus University, Washington University in St. Louis และ Uber ในเปเปอร์ชื่อ Dynamic Partial Deadlock Detection and Recovery via Garbage Collection ซึ่งนำเสนอในงานประชุมวิชาการ ASPLOS 2025 ต่อมาทีม Go นำแนวคิดนี้ไปพัฒนาต่อจนกลายเป็นฟีเจอร์ที่ใช้งานได้จริงในตัวภาษา
ข้อจำกัด 3 ข้อ และผลกระทบต่อประสิทธิภาพ
แม้ฟีเจอร์นี้จะมีประโยชน์มาก แต่ก็มีข้อจำกัดที่ควรทราบอยู่ 3 ข้อ:
- GC อาจมองขอบเขตการเข้าถึงกว้างเกินไป: ถ้ายังมีตัวแปรระดับ global หรือ goroutine อื่นที่ทำงานอยู่อ้างอิง primitive ตัวที่ทำให้ค้าง ตัวที่ค้างอยู่จะไม่ถูกนับว่ารั่ว แม้ว่าในความเป็นจริงโค้ดส่วนอื่นจะไม่มีวันกลับมาเรียกใช้งานมันอีกแล้วก็ตาม ทางแก้คือจำกัดขอบเขตการเข้าถึง primitive เหล่านี้ให้แคบลง และกำหนดวงจรชีวิตให้ชัดเจน
- รองรับเฉพาะ primitive มาตรฐานของ Go เท่านั้น: profiler ตัวนี้จะไม่ตรวจจับ goroutine ที่ติดค้างจาก I/O เช่น อ่านไฟล์ รอ network socket หรือเรียกคำสั่งระบบปฏิบัติการผ่าน syscall ส่วน concurrency primitive ที่ทีมเขียนขึ้นใช้เองก็อยู่นอกขอบเขต เว้นแต่ว่าภายในจะใช้ primitive มาตรฐานของ Go เป็นพื้นฐานอีกชั้นหนึ่ง
- ตรวจจับได้เฉพาะปัญหาที่เกิดขึ้นแล้วเท่านั้น: profiler ไม่สามารถพยากรณ์ล่วงหน้าได้ ถ้าบั๊กเกิดขึ้นเป็นพักๆ หรือเกิดเฉพาะบางเงื่อนไข ก็อาจจะจับได้ยาก บทความจึงแนะนำให้ใช้เครื่องมือร่วมกันหลายระดับ ทั้งชุดเทสต์ที่ใช้
goleakและsynctestควบคู่ไปกับการเปิด leak profile ตั้งแต่ระดับเทสต์ไปจนถึง production เพราะฟีเจอร์ใหม่นี้ออกแบบมาเพื่อเสริม ไม่ได้มาแทนที่เครื่องมือเดิม
ผลกระทบต่อประสิทธิภาพ: ด้านหน่วยความจำ ตัว profiler ใช้หน่วยความจำเพิ่มเพียงเล็กน้อยเพื่อบันทึกสถานะเท่านั้น แต่ด้านเวลาประมวลผล รอบการตรวจจับ leak อาจช้ากว่ารอบ GC ปกติ เนื่องจากขั้นตอนตรวจสอบ liveness ต้องรอผลลัพธ์ต่อเนื่องกันเป็นลูกโซ่ จึงต้องประมวลผลแบบเรียงลำดับแทนที่จะทำแบบขนานได้ทั้งหมด อีกทั้งการต้องวนลูปตรวจสอบ goroutine ที่ค้างซ้ำในแต่ละรอบ ทำให้ในกรณีที่แย่ที่สุด จำนวนขั้นตอนจะเพิ่มขึ้นเป็นกำลังสองของจำนวน goroutine ทั้งหมด
แต่บทความชี้ทางออกไว้ว่า leak ที่เกิดขึ้นแล้วจะยังคงค้างอยู่ให้ตรวจจับได้ตลอดในการรันรอบนั้น เราจึงตั้งความถี่การดึง profile ให้ห่างได้ เช่น ทุก 4 ชั่วโมง ซึ่งช่วยลดภาระของระบบลง โดยแทบไม่เสียความสามารถในการตรวจจับไปเลย
บอกตำแหน่งที่รั่ว แต่ไม่บอกสาเหตุ
สิ่งที่ต้องเข้าใจก่อนคือ ตัว profiler บอกเราได้ว่า goroutine ค้างอยู่ที่ฟังก์ชันไหนและบรรทัดใด แต่ไม่ได้บอก สาเหตุ ว่าทำไมมันถึงค้าง เช่น กรณีที่ส่งข้อมูลซ้ำสองรอบเพราะลืมเขียน return ตัว profiler จะชี้แค่ว่า goroutine กำลังค้างอยู่ที่บรรทัดส่งข้อมูลเข้า channel เท่านั้น หน้าที่ไล่ดูโค้ดเพื่อหาสาเหตุที่แท้จริงและวิธีแก้ไข ยังคงเป็นของนักพัฒนาเช่นเดิม
ประเด็นสำคัญอีกเรื่องคือ จำนวน goroutine ที่เห็นเยอะๆ ในระบบ อาจไม่ได้แปลว่ามี goroutine leak เสมอไป เพราะอาจเป็นพฤติกรรมปกติของระบบที่กำลังรับโหลดสูง ซึ่งความยากในการแยกแยะตรงนี้นี่เอง ที่ทำให้ทีม Go ต้องพัฒนาเครื่องมือตัวนี้ขึ้นมา
เครื่องมือตัวนี้ไม่ได้มาอ่านโค้ดแทนเรา แค่ช่วยตีวงจากโค้ดทั้งโปรเจกต์ให้เหลือไม่กี่บรรทัด และสำหรับระบบที่รันต่อเนื่องมาเป็นปี เบาะแสไม่กี่บรรทัดนี้คือสิ่งที่ขาดหายไปตลอด
ที่มา: บทความ Goroutine Leak Profiles จาก The Go Blog
ชอบเรื่องแนวนี้ มีอีบุ๊คฟรีให้อ่านต่อ
ChatGPT Work ฉบับเข้าใจง่าย มอบงานให้ AI ทำจนจบ ตั้งแต่งานแรกจนถึงงานอัตโนมัติ พร้อม workflow ใช้ได้จริง 8 แบบ
กดสมัครแล้วเราจะส่งเทคนิค AI และของแจกใหม่ๆ ให้ทางอีเมล เลิกรับได้ตลอด
Claude Cowork · The Business Playbook

ฉบับภาษาไทย 15 บท เรียนรู้ผ่านโปรเจกต์จำลองต่อเนื่องทั้งเล่ม ตั้งแต่ตั้งค่า Workspace จัดการไฟล์ เชื่อมแอป ตั้งระบบอัตโนมัติ จนถึงสร้าง Plugin


