[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"academy-blogs-th-1-1-all-golang-the-series-ep148-handling-ai-streams-go-channels-all--*":3,"academy-blog-translations-daytv9s90w0e91h":88},{"data":4,"page":75,"perPage":75,"totalItems":75,"totalPages":75},[5],{"alt":6,"collectionId":7,"collectionName":8,"content":9,"cover_image":10,"cover_image_path":11,"created":12,"created_by":13,"expand":14,"id":83,"keywords":84,"locale":56,"published_at":13,"scheduled_at":13,"school_blog":79,"short_description":85,"status":77,"title":86,"updated":87,"updated_by":13,"slug":80,"views":82},"การใช้ Go Channels และ Goroutines ในการจัดการ Stream Chunks จาก AI API","sclblg987654321","school_blog_translations","\u003Cp>ยินดีต้อนรับเข้าสู่ EP.148 ครับ! ในตอนที่แล้วเราได้เรียนรู้วิธีรับข้อมูลแบบ Structured JSON ที่แม่นยำ 100% ไปแล้ว แต่ในการใช้งานจริงเรามักจะเจอกับปัญหาใหญ่คือ \u003Cstrong>\"ความช้า\" (Latency)\u003C\u002Fstrong> เพราะยิ่งเราสั่งให้ AI วิเคราะห์เยอะ การรอให้มันประมวลผลจนจบก้อน JSON อาจใช้เวลานานหลายวินาที จนผู้ใช้งานนึกว่าระบบค้างหรือแอปพังไปแล้ว\u003C\u002Fp>\u003Cp>วันนี้เราจะมาแก้ปัญหานี้ด้วยการดึงจุดแข็งที่สุดของภาษา Go นั่นคือ \u003Cstrong>Channels\u003C\u002Fstrong> และ \u003Cstrong>Concurrency\u003C\u002Fstrong> มาจัดการกับระบบ \u003Cstrong>Streaming\u003C\u002Fstrong> เพื่อส่งข้อมูลแบบชิ้นเล็กๆ (Chunks) ทยอยแสดงผลออกมาแบบ Real-time ทันทีที่ AI เริ่มคิดคำแรกได้ ให้ความรู้สึกลื่นไหลเหมือนหน้าจอ ChatGPT ที่เราคุ้นเคยนั่นเองครับ\u003C\u002Fp>\u003Ch2>ทำไมต้องใช้ Go Channels ในการจัดการ Stream?\u003C\u002Fh2>\u003Cp>เมื่อเราเปิดโหมด Stream กับ AI API (ไม่ว่าจะเป็น OpenAI หรือ Ollama) พฤติกรรมการส่งข้อมูลจะเปลี่ยนไป จากเดิมที่มาเป็นก้อนใหญ่ก้อนเดียว จะเปลี่ยนเป็นการส่ง Chunks หรือเศษเสี้ยวของข้อความทยอยไหลมาตาม Network Connection ครับ\u003C\u002Fp>\u003Cp>การใช้ Go Channels เข้ามาเป็นท่อกลางในการรับ-ส่งข้อมูลชิ้นเล็กๆ เหล่านี้ มีข้อดีที่เหนือกว่าการเขียนลูปแบบปกติ 3 ด้าน:\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>Decoupling (การแยกส่วน):\u003C\u002Fstrong> ช่วยให้เราแยกหน้าที่กันชัดเจน (Separation of Concerns) ฝั่งหนึ่งมีหน้าที่แค่ดึงข้อมูลจาก AI ส่วนอีกฝั่งมีหน้าที่แค่แสดงผลให้ User ทั้งสองฝั่งไม่ต้องรู้จักกัน แค่คุยกันผ่าน Channel ก็พอ\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>Non-blocking (ทำงานไม่ขัดจังหวะ):\u003C\u002Fstrong> Go สามารถประมวลผลข้อมูลชิ้นที่ 1 (เช่น ส่งไปแสดงที่หน้าจอ) ไปพร้อมๆ กับการรอรับข้อมูลชิ้นที่ 2 จาก Network ได้ทันทีผ่านการทำงานแบบ Concurrency ทำให้ไม่มีช่วงเวลาที่ระบบต้อง \"หยุดรอ\" โดยเปล่าประโยชน์\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>Type Safety (ความปลอดภัยของข้อมูล):\u003C\u002Fstrong> เราสามารถกำหนดได้ว่า Channel นี้จะรับข้อมูลประเภทไหน (เช่น \u003Ccode>chan string\u003C\u002Fcode> หรือ \u003Ccode>chan MyStruct\u003C\u002Fcode>) ทำให้มั่นใจได้ว่าข้อมูลที่ไหลอยู่ในท่อจะถูกต้องตาม Schema ที่เราออกแบบไว้เสมอ ลดโอกาสเกิด Runtime Error\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Ch3>💡 Insight สำหรับ Gopher:\u003C\u002Fh3>\u003Cp>การใช้ Channel ในบริบทนี้เปรียบเสมือนการสร้างสายพานในโรงงานครับ API คือเครื่องจักรต้นทางที่วางของลงมาเรื่อยๆ และ UI คือพนักงานปลายทางที่คอยหยิบของไปโชว์ ถ้าไม่มีสายพาน (Channel) นี้ เราก็ต้องรอให้เครื่องจักรผลิตเสร็จทั้งหมดก่อนถึงจะเดินไปหยิบมาทีเดียว ซึ่งนั่นคือสาเหตุของความช้านั่นเอง\u003C\u002Fp>\u003Ch2>โครงสร้างการเขียน Stream ด้วย Go\u003C\u002Fh2>\u003Cp>เราจะสร้างฟังก์ชันที่ทำหน้าที่เป็นผู้ผลิต (Producer) โดยจะส่งค่ากลับไปเป็น \u003Cstrong>Receive-only Channel (\u003Ccode>&lt;-chan\u003C\u002Fcode>)\u003C\u002Fstrong> ซึ่งเปรียบเสมือนการส่งมอบท่อที่ปลายทางเอาไว้รอรับข้อมูลได้อย่างเดียว ป้องกันการส่งข้อมูลย้อนกลับผิดฝั่งครับ\u003C\u002Fp>\u003Ch3>ตัวอย่างการ Implement:\u003C\u002Fh3>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>\u002F\u002F ส่งคืนค่าเป็น &lt;-chan เพื่อบอกว่า \"ท่อนี้เอาไว้รับข้อมูลออกไปใช้อย่างเดียว\"\nfunc StreamAIResponse(ctx context.Context, client *openai.Client, prompt string) &lt;-chan string {\n    out := make(chan string)\n\n    \u002F\u002F รัน Goroutine แยกออกไปเพื่อไม่ให้ Block การทำงานของฟังก์ชันหลัก\n    go func() {\n        \u002F\u002F สำคัญมาก: ต้องปิด Channel เมื่อจบงานเสมอ เพื่อบอกปลายทางว่า \"ข้อมูลหมดแล้ว\"\n        defer close(out) \n        \n        req := openai.ChatCompletionRequest{\n            Model:  openai.GPT4o,\n            Stream: true, \u002F\u002F จุดเปลี่ยนสำคัญ: เปิดโหมด Streaming ให้ API ค่อยๆ พ่นข้อมูลออกมา\n            Messages: []openai.ChatCompletionMessage{\n                {Role: \"user\", Content: prompt},\n            },\n        }\n\n        stream, err := client.CreateChatCompletionStream(ctx, req)\n        if err != nil {\n            \u002F\u002F ในระดับโปรดักชัน ควรส่ง Error ออกไปทาง Channel หรือใช้ Logging\n            return \n        }\n        defer stream.Close()\n\n        for {\n            response, err := stream.Recv()\n            \u002F\u002F io.EOF คือสัญญาณบอกว่า AI พูดจบประโยคแล้ว\n            if errors.Is(err, io.EOF) {\n                return \n            }\n            if err != nil {\n                return\n            }\n            \n            \u002F\u002F ส่งข้อมูล \"ส่วนต่าง\" (Delta) ที่ได้เพิ่มมาเข้าท่อไปทันที\n            if len(response.Choices) &gt; 0 {\n                out &lt;- response.Choices[0].Delta.Content\n            }\n        }\n    }()\n\n    return out\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>🔍 เจาะลึกสิ่งที่เกิดขึ้นใน Code:\u003C\u002Fh3>\u003Col>\u003Cli>\u003Cp>\u003Cstrong>\u003Ccode>go func() { ... }()\u003C\u002Fcode>\u003C\u002Fstrong>: เราแยกการรอรับข้อมูลจาก API ไปไว้ใน Background (Goroutine) เพื่อให้ฟังก์ชัน \u003Ccode>StreamAIResponse\u003C\u002Fcode> คืนค่า Channel กลับไปให้ผู้ใช้ได้ทันทีโดยไม่ต้องรอ AI ตอบเสร็จ\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>\u003Ccode>defer close(out)\u003C\u002Fcode>\u003C\u002Fstrong>: นี่คือมารยาทที่ดีของการใช้ Channel หากเราไม่ปิดท่อ ปลายทางที่รอรับข้อมูลด้วย \u003Ccode>range\u003C\u002Fcode> จะค้างอยู่แบบนั้นตลอดไป (Deadlock)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>\u003Ccode>stream.Recv()\u003C\u002Fcode>\u003C\u002Fstrong>: ฟังก์ชันนี้จะ \"Block\" การทำงานภายใน Goroutine เพื่อรอรับข้อมูลชิ้นถัดไปจาก AI เมื่อได้มาแล้วเราก็รีบส่งเข้าท่อ \u003Ccode>out &lt;-\u003C\u002Fcode> ทันที\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Fol>\u003Ch2>การนำไปใช้งาน (Consumption)\u003C\u002Fh2>\u003Cp>ที่ฝั่ง \u003Cstrong>Main\u003C\u002Fstrong> หรือ \u003Cstrong>Controller\u003C\u002Fstrong> (ปลายทางของท่อ) เราไม่จำเป็นต้องรู้เรื่องความซับซ้อนของ API หรือการจัดการ Goroutine เลยครับ หน้าที่เดียวของเราคือการรอรับข้อมูลที่ไหลออกมาจาก Channel โดยใช้ keyword \u003Ccode>for range\u003C\u002Fcode>\u003C\u002Fp>\u003Ch3>ตัวอย่างการเรียกใช้งาน:\u003C\u002Fh3>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>\u002F\u002F 1. เรียกใช้งานฟังก์ชันและรับ Channel กลับมา\nresponseChan := StreamAIResponse(ctx, client, \"ช่วยอธิบายเรื่อง Go Channels หน่อย\")\n\n\u002F\u002F 2. ใช้ range ในการดึงข้อมูลออกจากท่อจนกว่า Channel จะถูกสั่ง close()\nfor msg := range responseChan {\n    \u002F\u002F 3. แสดงผลทีละตัวอักษร\u002Fคำ ทันทีที่ได้รับข้อมูลมา\n    fmt.Print(msg) \n}\n\nfmt.Println(\"\\n--- จบการทำ Streaming ---\")\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>ทำไมวิธีนี้ถึงยอดเยี่ยม?\u003C\u002Fh3>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>ความลื่นไหล (Smooth UX):\u003C\u002Fstrong> ข้อความจะไม่กระตุกหรือรอมาทีเดียวเป็นก้อน แต่จะค่อยๆ ปรากฏบนจอ (Typing Effect) เหมือนมีคนกำลังพิมพ์อยู่จริงๆ ซึ่งช่วยลดความรู้สึกหงุดหงิดของผู้ใช้งานเวลาต้องรอ AI คิดนานๆ\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>จัดการหน่วยความจำได้ดี:\u003C\u002Fstrong> เราไม่ต้องเก็บ String ยาวๆ ไว้ใน Memory จนจบการทำงาน แต่เราประมวลผล (ในที่นี้คือการพิมพ์ออกจอ) ไปได้เลยทีละนิด\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>Loop จะหยุดเองอัตโนมัติ:\u003C\u002Fstrong> เมื่อ Goroutine ฝั่งต้นทางทำงานเสร็จและสั่ง \u003Ccode>close(out)\u003C\u002Fcode> ลูป \u003Ccode>for range\u003C\u002Fcode> นี้จะหลุดออกมาเองอย่างปลอดภัย (Graceful Exit) โดยที่เราไม่ต้องเขียนเงื่อนไขหยุดให้วุ่นวายครับ\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>ข้อควรระวังในการทำ Stream\u003C\u002Fh2>\u003Cp>ถึงแม้ Go จะจัดการ Concurrency ได้ดีมาก แต่เราในฐานะ Developer ต้องระวัง 2 เรื่องหลักเพื่อความปลอดภัยของระบบ:\u003C\u002Fp>\u003Ch3>1. Context Cancellation (การยกเลิกคำสั่ง)\u003C\u002Fh3>\u003Cp>นี่คือจุดที่พลาดกันบ่อยที่สุดครับ! หาก User กดหยุด (Stop) หรือปิดหน้า Browser ทิ้งไปในขณะที่ AI กำลังพ่นคำออกมา ถ้าเราไม่จัดการส่ง \u003Ccode>ctx\u003C\u002Fcode> (Context) เข้าไปในฟังก์ชัน \u003Ccode>CreateChatCompletionStream\u003C\u002Fcode> ตัว Backend ของเราจะยังคงรันลูปและรับข้อมูลจาก AI ต่อไปเรื่อยๆ จนจบ\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>ผลเสีย:\u003C\u002Fstrong> เสีย Token ฟรี (เสียเงิน!), Server ทำงานหนักโดยไม่มีใครดูผลลัพธ์\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>วิธีแก้:\u003C\u002Fstrong> ตรวจสอบเสมอว่า \u003Ccode>ctx\u003C\u002Fcode> ยังใช้งานได้อยู่หรือไม่ และต้องมั่นใจว่า \u003Ccode>stream.Close()\u003C\u002Fcode> จะถูกเรียกทันทีเมื่อ Context ถูกยกเลิก\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Ch3>2. Buffer Size &amp; Backpressure (การจัดการแรงดันข้อมูล)\u003C\u002Fh3>\u003Cp>โดยปกติการทำ Chat เรามักใช้ \u003Cstrong>Unbuffered Channel\u003C\u002Fstrong> (ขนาด 0) เพื่อความสดใหม่ของข้อมูล เพราะเราอยากให้ข้อมูลไหลจาก API ถึงหน้าจอ User ทันทีโดยไม่ค้างอยู่ในท่อ\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>เมื่อไหร่ควรใช้ Buffered Channel?:\u003C\u002Fstrong> หากฝั่ง \"ผู้รับ\" (Consumer) ทำงานช้ากว่าฝั่งผู้ส่ง (เช่น ต้องเอาข้อมูลไปผ่านฟิลเตอร์บางอย่างก่อนโชว์) การใช้ Buffered Channel (เช่น \u003Ccode>make(chan string, 10)\u003C\u002Fcode>) จะช่วยให้ฝั่งส่งไม่ต้องหยุดรอฝั่งรับจนเกินไป ช่วยลดอาการกระตุกของ Stream ได้ครับ\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Ch3>💡 สรุป Rule of Thumb สำหรับ Gopher:\u003C\u002Fh3>\u003Cblockquote>\u003Cp>\u003Cstrong>\"เปิดได้ ต้องปิดเป็น\"\u003C\u002Fstrong> — ทุกครั้งที่ใช้ \u003Ccode>go func\u003C\u002Fcode> และ \u003Ccode>channel\u003C\u002Fcode> ต้องตอบคำถามให้ได้เสมอว่า Goroutine นี้จะจบการทำงานเมื่อไหร่ และ Channel นี้จะถูกปิดตอนไหน ถ้าหาคำตอบไม่ได้ มีโอกาสสูงที่จะเกิด \u003Cstrong>Goroutine Leak\u003C\u002Fstrong> ซึ่งจะกัดกิน Memory ของ Server คุณไปเรื่อยๆ ครับ\u003C\u002Fp>\u003C\u002Fblockquote>\u003Ch2>🎯 ท้าให้ลอง (Daily Mission)\u003C\u002Fh2>\u003Cp>เพื่อให้คุณเข้าใจการไหลของข้อมูลได้ลึกซึ้งขึ้น ผมอยากให้ลองอัปเกรดฟังก์ชัน Stream ให้มีความเป็นมืออาชีพมากขึ้น โดยไม่ได้ส่งแค่ข้อความดิบๆ แต่ให้ส่งข้อมูลที่มีบริบทครบถ้วนผ่าน Channel ครับ\u003C\u002Fp>\u003Cp>\u003Cstrong>โจทย์:\u003C\u002Fstrong> ลองสร้าง Struct สำหรับส่งข้อมูล และปรับปรุงฟังก์ชันให้ส่งค่าผ่าน Channel ดังนี้:\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>type StreamResponse struct {\n    Content      string\n    FinishReason string\n    Err          error\n}\n\n\u002F\u002F การบ้าน: ลองปรับ StreamAIResponse ให้คืนค่าเป็น &lt;-chan StreamResponse\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>คำถามชวนคิด:\u003C\u002Fh3>\u003Cp>ลองจับเวลา (Benchmark) ในใจดูครับว่า ระหว่าง:\u003C\u002Fp>\u003Col>\u003Cli>\u003Cp>\u003Cstrong>Standard Mode:\u003C\u002Fstrong> รอ 5 วินาที แล้วข้อความยาวๆ โผล่มาทีเดียว\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>Streaming Mode:\u003C\u002Fstrong> รอ 0.5 วินาที แล้วคำแรกเริ่มพ่นออกมาเรื่อยๆ จนจบในวินาทีที่ 5\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Fol>\u003Cp>คุณคิดว่าผู้ใช้งานจะกด Refresh หรือปิดแอปทิ้งในกรณีไหนมากกว่ากัน? นี่คือสิ่งที่เรียกว่า \u003Cstrong>Perceived Performance\u003C\u002Fstrong> หรือความเร็วที่ใจของคนใช้รู้สึก ซึ่งสำคัญพอๆ กับความเร็วของ Code เลยครับ\u003C\u002Fp>\u003Cdiv data-type=\"horizontalRule\">\u003Chr>\u003C\u002Fdiv>\u003Ch2>สรุป\u003C\u002Fh2>\u003Cp>การทำ \u003Cstrong>Streaming\u003C\u002Fstrong> ด้วย Go Channels เปลี่ยนประสบการณ์จากแอปที่ดูนิ่งค้างให้กลายเป็นแอปที่มีชีวิต การจัดการ Concurrency ที่สะอาดตาของ Go ช่วยให้เราเขียนระบบที่ดูเหมือนจะซับซ้อนให้กลายเป็นเรื่องง่ายและปลอดภัย\u003C\u002Fp>\u003Cp>แต่ความเร็วที่เพิ่มขึ้นย่อมมาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง...\u003C\u002Fp>\u003Ch3>ในตอนต่อไป (EP.149):\u003C\u002Fh3>\u003Cp>เมื่อระบบไหลลื่น ผู้ใช้แฮปปี้ แต่สิ่งที่อาจทำให้คุณไม่แฮปปี้คือบิลค่า API ตอนสิ้นเดือนครับ! เราจะมาดูวิธีควบคุมงบประมาณแบบเข้มข้นในตอน \u003Cstrong>\"Token Management: วิธีนับ Token และคำนวณต้นทุน API ในฝั่ง Backend\"\u003C\u002Fstrong>\u003C\u002Fp>\u003Cp>เราจะมาเจาะลึกกันว่า AI มองตัวอักษรของเราเป็นเลขอะไร? และเราจะเดาใจค่าใช้จ่ายก่อนส่งไปหา API ได้อย่างไร? ใครไม่อยากให้บิลปลายเดือนช็อก... ห้ามพลาดครับ!\u003C\u002Fp>\u003Cp>\u003Cstrong>เจอกันตอนหน้าครับ Gophers!\u003C\u002Fstrong>\u003C\u002Fp>\u003Cp>\u003Cstrong>ฝากกดติดตามพวกเราได้ที่ Superdev Academy\u003C\u002Fstrong> ในทุกช่องทางนะครับ!\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>🔵 Facebook: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.facebook.com\u002Fsuperdev.academy.th\">\u003Cstrong>Superdev Academy Thailand\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(อัปเดตข่าวสารและบทความใหม่)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>🎬 YouTube: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.youtube.com\u002F@SuperdevAcademy\">\u003Cstrong>Superdev Academy Channel\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(ติวเข้มแบบวิดีโอ)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>📸 Instagram: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.instagram.com\u002Fsuperdevacademy\u002F\">\u003Cstrong>@superdevacademy\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(เกร็ดความรู้สั้นๆ และเบื้องหลังการทำงาน)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>🎬 TikTok: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.tiktok.com\u002F@superdevacademy?lang=th-TH\">\u003Cstrong>@superdevacademy\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(Tips &amp; Tricks ฉบับย่อยง่าย)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>🌐 Website: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"http:\u002F\u002Fsuperdevacademy.com\">\u003Cstrong>superdevacademy.com\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(คลังบทความและคอร์สเรียนฉบับเต็ม)\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Cp>\u003C\u002Fp>","15a3dvr004p9_1qvtta1iku.png","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002Fx1twyuwru1r2m2q\u002F15a3dvr004p9_1qvtta1iku.png","2026-05-19 09:10:02.263Z","",{"keywords":15,"locale":50,"school_blog":60},[16,22,27,32,37,41,46],{"collectionId":17,"collectionName":18,"created":19,"created_by":13,"id":20,"name":21,"updated":19,"updated_by":13},"sclkey987654321","school_keywords","2026-05-19 09:09:15.823Z","fbj34lco59k2lc0","Go Channels",{"collectionId":17,"collectionName":18,"created":23,"created_by":13,"id":24,"name":25,"updated":26,"updated_by":13},"2026-03-04 08:20:11.547Z","ey3puyme01a9bsw","Go","2026-04-10 16:07:25.893Z",{"collectionId":17,"collectionName":18,"created":28,"created_by":13,"id":29,"name":30,"updated":31,"updated_by":13},"2026-03-04 08:20:14.253Z","ah6lvy4x8qe08l5","Golang","2026-04-10 16:07:26.172Z",{"collectionId":17,"collectionName":18,"created":33,"created_by":13,"id":34,"name":35,"updated":36,"updated_by":13},"2026-03-04 08:24:48.143Z","dourw0uuydrrh1h","Concurrency","2026-04-10 16:07:30.157Z",{"collectionId":17,"collectionName":18,"created":38,"created_by":13,"id":39,"name":40,"updated":38,"updated_by":13},"2026-05-19 09:09:41.882Z","rx514ns8m8ei8tl","AI Streaming",{"collectionId":17,"collectionName":18,"created":42,"created_by":13,"id":43,"name":44,"updated":45,"updated_by":13},"2026-03-04 08:33:58.044Z","nb6p1r8sfqlsxf8","Goroutines","2026-04-10 16:08:04.493Z",{"collectionId":17,"collectionName":18,"created":47,"created_by":13,"id":48,"name":49,"updated":47,"updated_by":13},"2026-05-19 09:09:59.339Z","qic0tqri4x8tvnf","Backend Latency",{"code":51,"collectionId":52,"collectionName":53,"created":54,"flag":55,"id":56,"is_default":57,"label":58,"updated":59},"th","pbc_1989393366","locales","2026-01-22 10:59:55.832Z","twemoji:flag-thailand","s8wri3bt4vgg2ji",true,"Thai","2026-04-10 15:42:46.614Z",{"category":61,"collectionId":62,"collectionName":63,"created":64,"expand":65,"id":79,"slug":80,"updated":81,"views":82},"wqxt7ag2gn7xcmk","pbc_2105096300","school_blogs","2026-05-19 09:10:01.807Z",{"category":66},{"blogIds":67,"collectionId":68,"collectionName":69,"created":70,"created_by":13,"id":61,"image":71,"image_alt":13,"image_path":72,"label":73,"name":74,"priority":75,"publish_at":76,"scheduled_at":13,"status":77,"updated":78,"updated_by":13},[],"sclcatblg987654321","school_category_blogs","2026-03-04 08:33:53.210Z","59ty92ns80w_15oc1implw.png","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclcatblg987654321\u002Fwqxt7ag2gn7xcmk\u002F59ty92ns80w_15oc1implw.png",{"en":74,"th":74},"Golang The Series",1,"2026-03-16 04:39:38.440Z","published","2026-04-25 02:32:15.470Z","daytv9s90w0e91h","golang-the-series-ep148-handling-ai-streams-go-channels","2026-06-02 01:24:13.020Z",107,"x1twyuwru1r2m2q",[20,24,29,34,39,43,48],"แก้ปัญหา AI ตอบช้าด้วยเทคนิค Streaming! เรียนรู้วิธีใช้ Go Channels และ Concurrency เพื่อทยอยส่งข้อมูลแบบชิ้นเล็กๆ (Chunks) ให้ผู้ใช้เห็นผลลัพธ์ทันทีเหมือน ChatGPT","Golang The Series EP.148: Handling Streams - สร้างระบบ Chat Real-time ด้วย Go Channels","2026-06-02 01:24:05.397Z",{"th":80,"en":80}]