⚡ Go Senior Mentor Engine | Phase 1: Week 1 - Day 5 (Master Edition)
Score: 0 / 15
Answered: 0 / 15
Accuracy: 0%
Phase 1: Week 1 String Internals Master Edition (15 Questions + 3 Labs)

Day 5: Strings, Runes & Byte Buffers (Zero-Copy Conversions)

เจาะลึกโครงสร้างข้อมูลข้อความระดับหน่วยความจำ: ชำแหละ StringHeader (16 ไบต์), ความแตกต่างระหว่าง Byte vs Rune (UTF-8 Code Point), อันตรายของการแปลง `string([]byte)` ในระบบ Ingestion สตรีมธุรกรรม และเทคนิคการสร้าง Buffer ปลอด Allocation ด้วย `strings.Builder` และ `unsafe`

0. The "Why": ทำไมการจัดการ String ใน Go ถึงไม่เหมือน Java/Kotlin?

ในฝั่ง JVM เมื่อคุณเขียน val message = "Hello" ตัวแปร message คือ Reference ชี้ไปยัง Object ขนาดใหญ่บน Heap ที่มี Object Header (12-16 ไบต์), ตัวแปร int hash, และ Reference ไปยัง byte[] หลังบ้าน และเมื่อคุณทำการเชื่อมสตริงด้วยเครื่องหมายบวก (str1 + str2) หรือทำ Substring ในสมัยก่อน JVM อาจสร้าง Object ขยะขึ้นมาใหม่ใน Eden Space เสมอ

ในระบบ Core Banking Payment Gateway หรือ High-Frequency RFID Ingestion ที่รับข้อมูลวินาทีละ 100,000 รายการ การแปลงสตริงไปมาระหว่าง Network Packet ([]byte) กับ Domain Model (string) หากทำไม่ระวังจะทำให้เกิด Heap Allocation มหาศาล ส่งผลให้ Go Garbage Collector ต้องตื่นขึ้นมา Scavenge หน่วยความจำ จนเกิด Latency Spike ระดับมิลลิวินาที

คำศัพท์เทคนิคระดับวิศวกรรมสถาปัตยกรรม (Vocabulary):

  • String (สตริง): ใน Go สตริงคือ Read-Only Slice of Bytes มีโครงสร้างหัว (Header) ขนาด 16 ไบต์บน 64-bit และข้อมูลเนื้อหาใน RAM เป็น Immutable ห้ามแก้ไขไบต์ภายในเด็ดขาด
  • Rune (รูน): เป็น Alias (ชื่อเรียกแทน) ของ int32 ขนาด 4 ไบต์ ใช้แทน 1 Unicode Code Point (ตัวอักษร 1 ตัวในตารางยูนิโค้ด เช่น ตัวอักษรไทย 1 ตัวใช้ UTF-8 ขนาด 3 ไบต์ แต่ถูกเก็บใน Rune ได้พอดี)
  • UTF-8 Encoding: รูปแบบการเข้ารหัสข้อความแบบ Variable-length (1 ถึง 4 ไบต์ต่อตัวอักษร) สตริงของ Go ถูกเก็บเป็น UTF-8 Bytes เสมอ
  • Heap Allocation Overhead: เมื่อคุณแปลง string(byteSlice) หรือ []byte(str) ตัว Go Runtime จะต้องทำการ คัดลอกข้อมูลทั้งหมดซ้ำลงใน Memory ก้อนใหม่ (Defensive Copy) เสมอเพื่อรักษาความปลอดภัยว่าข้อมูลจะไม่ถูกแก้
  • Zero-Copy Conversion: เทคนิคระดับสูงในการหลอก Compiler ให้แชร์ Backing Array ระหว่าง string กับ []byte ร่วมกัน โดยไม่ต้องจอง RAM ใหม่และไม่เกิดการคัดลอกแม้แต่ไบต์เดียว
1. Go Syntax & Command Blueprint: คู่มือไวยากรณ์ String, Rune และ Builder

เรียนรู้วิธีการเขียนคำสั่งและรูปแบบไวยากรณ์ที่ใช้จัดการข้อความใน Go เทียบกับ Kotlin/Java:

1.1 การสร้างสตริงและการพิมพ์ข้อความ (fmt Package)

package main

import "fmt"

func main() {
    // 1. Double Quotes: สตริงปกติ มีการตีความ Escape Characters เช่น \n, \t
    normalStr := "THB: 1500.50\n"

    // 2. Backticks (Raw String Literal): สตริงดิบ ไม่ตีความ Escape และขึ้นบรรทัดใหม่ได้หลายบรรทัด
    jsonPayload := `{
        "account_id": "ACC-0911",
        "currency": "THB"
    }`

    // 3. การพิมพ์ผลลัพธ์ผ่าน fmt
    fmt.Print(normalStr)                                     // พิมพ์ตรงๆ ไม่ขึ้นบรรทัดใหม่เพิ่ม
    fmt.Println("Payment Stream Processed")                  // พิมพ์พร้อมขึ้นบรรทัดใหม่
    fmt.Printf("Account: %s, Payload: %s\n", "101", jsonPayload) // จัด Format ตาม Verb (%s, %d, %v)
}

1.2 ความแตกต่างระหว่าง Len() ของ Byte vs Rune (ตัวอักษรภาษาไทย)

คำสั่ง len(str) ใน Go จะคืนค่า **จำนวนไบต์ (Number of Bytes)** เสมอ ไม่ใช่จำนวนตัวอักษร!

import (
    "fmt"
    "unicode/utf8"
)

func CheckLength() {
    thaiText := "พร้อมเพย์" // แต่ละตัวอักษรภาษาไทยใน UTF-8 กินพื้นที่ 3 ไบต์

    // len() คืนค่าจำนวนไบต์จริงใน RAM
    fmt.Println("Bytes length:", len(thaiText)) // Output: 24 ไบต์! (8 ตัวอักษร x 3B)

    // ต้องการนับจำนวนตัวอักษรจริง (Unicode Code Points) ต้องใช้ utf8.RuneCountInString
    fmt.Println("Character count:", utf8.RuneCountInString(thaiText)) // Output: 8 ตัวอักษร
}

1.3 การต่อสตริงประสิทธิภาพสูงด้วย `strings.Builder`

ห้ามใช้เครื่องหมายบวก (+) ในลูปประมวลผลธุรกรรม ให้ใช้ strings.Builder แทน (เทียบเท่า StringBuilder ใน Java):

import (
    "fmt"
    "strings"
)

func BuildLedgerLog(entries []string) string {
    var builder strings.Builder

    // แนะนำอย่างยิ่ง: จองหน่วยความจำล่วงหน้าด้วย Grow() เพื่อหลีกเลี่ยงการ re-allocate
    builder.Grow(1024) 

    for _, entry := range entries {
        builder.WriteString(entry)
        builder.WriteByte('\n') // เร็วกว่า WriteString("\n") เพราะเขียนตรงระดับ 1 ไบต์
    }

    return builder.String() // Zero-allocation string extraction
}
2. Memory Architecture & Struct Definitions: ไส้ในของ String

2.1 โครงสร้าง StringHeader (16 Bytes)

ใน Go runtime (ไฟล์ src/runtime/string.go) สตริงไม่ใช่ Pointer เปล่าๆ และไม่ใช่ Object แต่คือ Struct ขนาด 16 ไบต์บนเครื่อง 64-bit:

type StringHeader struct {
    Data uintptr // 8 Bytes: ชี้ไปยังแอดเดรสของ Backing Byte Array บน RAM
    Len  int     // 8 Bytes: ความยาวของสตริงในหน่วยจำนวนไบต์ (ไม่ใช่ความจุ)
}

สังเกตว่า ไม่มีฟิลด์ `Cap` (Capacity) เหมือนใน `SliceHeader` เพราะสตริงถูกล็อกว่า Immutable ห้ามขยายหรือแก้ไขข้อมูลเดิมใน Backing Array

คุณสมบัติ Java `java.lang.String` Go `string` (StringHeader)
ขนาดของ Header Object บน Heap (~24-32 Bytes รวม Object Header + Coder + Hash) 16 Bytes คงที่บน Stack (Data Pointer 8B + Len 8B)
Encoding UTF-16 (ดั้งเดิม) หรือ Compact Strings (Latin-1/UTF-16) UTF-8 Bytes เสมอตามข้อกำหนดของภาษา
Memory Behavior สร้าง Object บน Heap ทุกครั้งถ้าไม่มี String Pool เป็น Value Type คัดลอกเพียง 16 ไบต์เมื่อส่งผ่านฟังก์ชัน
Indexing (`s[i]`) ได้ตัวอักษร char (UTF-16 Code Unit) ได้ uint8 (ไบต์ดิบที่ตำแหน่ง index นั้น ไม่ใช่ตัวอักษร!)
3. Visual Workflow & Zero-Copy Conversion Mechanics

3.1 การวนลูป: Indexing vs For-Range Loop

+---------------------------------------------------------------------------------------------------+ | STRING ITERATION BEHAVIOR IN RAM: str := "Aธ" (UTF-8: [ 0x41 ][ 0xE0 0xB8 0x98 ]) | +---------------------------------------------------------------------------------------------------+ | 1. วนลูปด้วย Index: for i := 0; i < len(str); i++ | | - เข้าถึงทีละ 1 ไบต์ดิบ (uint8) | | - i=0: 0x41 ('A'), i=1: 0xE0, i=2: 0xB8, i=3: 0x98 (ได้เศษไบต์ ไม่ใช่ตัว 'ธ'!) | | | | 2. วนลูปด้วย Range: for idx, r := range str | | - Go Runtime ถอดรหัส UTF-8 อัตโนมัติ (Rune Decoding) | | - รอบที่ 1: idx=0, r=65 ('A') | | - รอบที่ 2: idx=1, r=3608 ('ธ') ─── กระโดดข้าม index ไป 3 ไบต์ทันที! | +---------------------------------------------------------------------------------------------------+

3.2 ต้นทุนของ Defensive Copy: `string(b)` vs `[]byte(s)`

เมื่อคุณแปลงสลับไปมาระหว่าง Slice กับ String ปกติ:

  • b := []byte(s): Go ต้องจอง Heap Array ก้อนใหม่ขนาดเท่า len(s) แล้วคัดลอกข้อมูลทั้งหมด เพื่อป้องกันไม่ให้คนเอา b[0] = 'X' ไปแก้สตริง s ที่เป็น Immutable
  • s := string(b): Go ต้องจอง Backing Array ก้อนใหม่บน Heap แล้วคัดลอกไบต์จาก b เข้าไป เพื่อการันตีว่าถ้าใครมาแก้ b ภายหลัง สตริง s จะไม่ขยับตาม

3.3 พิมพ์เขียว Zero-Copy String Conversion (Go 1.20+)

ในระบบที่ต้องการความเร็วสูงระดับ Microsecond (เช่น Parsing FIX Protocol ของธนาคาร):

import (
    "unsafe"
)

// Zero-allocation: แปลง []byte เป็น string โดยแชร์ Backing Array ร่วมกัน
func BytesToStringZeroCopy(b []byte) string {
    if len(b) == 0 {
        return ""
    }
    // ใช้ unsafe.String ชี้ไปยังไบต์แรกของ Slice โดยไม่จอง RAM ใหม่
    return unsafe.String(unsafe.SliceData(b), len(b))
}

// Zero-allocation: แปลง string เป็น []byte แบบ Read-Only
func StringToBytesZeroCopy(s string) []byte {
    if len(s) == 0 {
        return nil
    }
    // ใช้ unsafe.Slice ชี้ไปยัง Data Pointer ของ StringHeader
    return unsafe.Slice(unsafe.StringData(s), len(s))
}
⚠️ กฎเหล็กความปลอดภัยของ Zero-Copy Conversion:
เมื่อคุณแปลง StringToBytesZeroCopy(s) คุณห้ามทำการแก้ไขไบต์ใน Slice นั้นเด็ดขาด (เช่น ห้าม b[0] = 'Z') เพราะตัวแปรต้นทางอยู่ใน Read-Only Memory ของ OS หากคุณพยายามเขียนทับ โปรแกรมจะพังทันทีด้วยข้อผิดพลาด Segmentation Fault (SIGSEGV) / Fatal Crash
4. Edge Cases, Hazards & Idiomatic Patterns

4.1 กับดัก Substring Memory Retention (Memory Leak ลับๆ)

พิจารณากรณีที่คุณรับ Payload ธุรกรรม ISO-8583 ขนาดใหญ่ (100 KB) เข้ามา แล้วต้องการตัดเอาเฉพาะ Transaction ID ขนาด 10 ไบต์:

func GetTxID(largePayload string) string {
    // BUG: txID เป็น StringHeader ใหม่ที่มี Len=10 
    // แต่ Data Pointer ยังคงชี้ไปยัง Backing Array ขนาด 100 KB เดิม!
    txID := largePayload[0:10]
    return txID 
}
⚠️ ผลลัพธ์ใน RAM: ตราบใดที่ตัวแปร txID ยังถูกถือครองอยู่ (เช่น ถูกเก็บใน In-Memory Cache) Backing Array ทั้งก้อนขนาด 100 KB จะไม่ถูก Garbage Collector เก็บกวาดเด็ดขาด! หากทำแบบนี้ 10,000 ครั้ง จะเกิด Memory Leak หลายกิกะไบต์
วิธีแก้: บังคับ Deep-Copy ด้วยฟังก์ชัน strings.Clone(txID) เพื่อตัดขาดความสัมพันธ์กับก้อนใหญ่ทันที

4.2 การใช้ Buffer Pool สำหรับ Ingestion Stream

ในระบบประมวลผลสูง แทนที่จะสร้าง bytes.Buffer ใหม่ทุกครั้ง ให้ใช้งานร่วมกับ sync.Pool เพื่อนำ Buffer เก่าที่ล้างข้อมูลแล้วกลับมาใช้ซ้ำ (Reuse) ตัดภาระ GC Allocation เหลือ 0

🧠 Knowledge Verification: Master 15-Question Matrix (Strings & Buffers)
🛠️ Hands-on Lab 1.5.1: Zero-Allocation Payment Audit Log Formatter
Banking Performance
📋 โจทย์ข้อกำหนด:
  1. เขียนฟังก์ชัน FormatAuditLog(txID string, fromAcc string, toAcc string, amount string) string
  2. ห้ามใช้เครื่องหมายบวก (+) หรือ fmt.Sprintf เพราะสร้าง Heap Overhead
  3. ต้องใช้ strings.Builder พร้อมคำนวณและสั่ง builder.Grow(...) ล่วงหน้าเพื่อตัด Heap Allocation ซ้ำซ้อน
  4. ผลลัพธ์ต้องเรียงในรูปแบบ: "TX: " + txID + " | FROM: " + fromAcc + " | TO: " + toAcc + " | AMT: " + amount
🛠️ Hands-on Lab 1.5.2: Safe Substring Extraction (Prevent Memory Leak)
Memory Retention Fix
📋 โจทย์ข้อกำหนด:
  1. ฟังก์ชัน ExtractSessionToken(rawPayload string) string รับสตริงก้อนใหญ่
  2. ตัดเอา 16 ตัวแรกออกมา (rawPayload[:16]) เพื่อเป็น Session Token
  3. ต้องใช้ strings.Clone เพื่อทำ Deep-Copy ตัดขาดจาก Backing Array ก้อนใหญ่ ป้องกัน Memory Retention Leak ในระบบ Long-lived Cache
🛠️ Hands-on Lab 1.5.3: Ultra-Fast Zero-Copy Byte-to-String Parser
Zero-Alloc Protocol Parser
📋 โจทย์ข้อกำหนด:
  1. เขียนฟังก์ชัน FastBytesToString(b []byte) string สำหรับแปลง Network Packet ที่รับจาก Socket
  2. ต้องใช้ฟังก์ชัน unsafe.String และ unsafe.SliceData (Go 1.20+) เพื่อคืนค่าสตริงโดยไม่เกิด Heap Allocation
  3. หาก slice มีขนาด 0 ต้องคืนค่า "" โดยปลอดภัย