ในฝั่ง 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 ระดับมิลลิวินาที
int32 ขนาด 4 ไบต์ ใช้แทน 1 Unicode Code Point (ตัวอักษร 1 ตัวในตารางยูนิโค้ด เช่น ตัวอักษรไทย 1 ตัวใช้ UTF-8 ขนาด 3 ไบต์ แต่ถูกเก็บใน Rune ได้พอดี)string(byteSlice) หรือ []byte(str) ตัว Go Runtime จะต้องทำการ คัดลอกข้อมูลทั้งหมดซ้ำลงใน Memory ก้อนใหม่ (Defensive Copy) เสมอเพื่อรักษาความปลอดภัยว่าข้อมูลจะไม่ถูกแก้string กับ []byte ร่วมกัน โดยไม่ต้องจอง RAM ใหม่และไม่เกิดการคัดลอกแม้แต่ไบต์เดียวเรียนรู้วิธีการเขียนคำสั่งและรูปแบบไวยากรณ์ที่ใช้จัดการข้อความใน Go เทียบกับ Kotlin/Java:
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)
}
คำสั่ง 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 ตัวอักษร
}
ห้ามใช้เครื่องหมายบวก (+) ในลูปประมวลผลธุรกรรม ให้ใช้ 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
}
ใน 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 นั้น ไม่ใช่ตัวอักษร!) |
เมื่อคุณแปลงสลับไปมาระหว่าง Slice กับ String ปกติ:
b := []byte(s): Go ต้องจอง Heap Array ก้อนใหม่ขนาดเท่า len(s) แล้วคัดลอกข้อมูลทั้งหมด เพื่อป้องกันไม่ให้คนเอา b[0] = 'X' ไปแก้สตริง s ที่เป็น Immutables := string(b): Go ต้องจอง Backing Array ก้อนใหม่บน Heap แล้วคัดลอกไบต์จาก b เข้าไป เพื่อการันตีว่าถ้าใครมาแก้ b ภายหลัง สตริง s จะไม่ขยับตามในระบบที่ต้องการความเร็วสูงระดับ 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))
}
StringToBytesZeroCopy(s) คุณห้ามทำการแก้ไขไบต์ใน Slice นั้นเด็ดขาด (เช่น ห้าม b[0] = 'Z') เพราะตัวแปรต้นทางอยู่ใน Read-Only Memory ของ OS หากคุณพยายามเขียนทับ โปรแกรมจะพังทันทีด้วยข้อผิดพลาด Segmentation Fault (SIGSEGV) / Fatal Crash
พิจารณากรณีที่คุณรับ 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
}
txID ยังถูกถือครองอยู่ (เช่น ถูกเก็บใน In-Memory Cache) Backing Array ทั้งก้อนขนาด 100 KB จะไม่ถูก Garbage Collector เก็บกวาดเด็ดขาด! หากทำแบบนี้ 10,000 ครั้ง จะเกิด Memory Leak หลายกิกะไบต์strings.Clone(txID) เพื่อตัดขาดความสัมพันธ์กับก้อนใหญ่ทันที
ในระบบประมวลผลสูง แทนที่จะสร้าง bytes.Buffer ใหม่ทุกครั้ง ให้ใช้งานร่วมกับ sync.Pool เพื่อนำ Buffer เก่าที่ล้างข้อมูลแล้วกลับมาใช้ซ้ำ (Reuse) ตัดภาระ GC Allocation เหลือ 0
FormatAuditLog(txID string, fromAcc string, toAcc string, amount string) string+) หรือ fmt.Sprintf เพราะสร้าง Heap Overheadstrings.Builder พร้อมคำนวณและสั่ง builder.Grow(...) ล่วงหน้าเพื่อตัด Heap Allocation ซ้ำซ้อน"TX: " + txID + " | FROM: " + fromAcc + " | TO: " + toAcc + " | AMT: " + amountExtractSessionToken(rawPayload string) string รับสตริงก้อนใหญ่rawPayload[:16]) เพื่อเป็น Session Tokenstrings.Clone เพื่อทำ Deep-Copy ตัดขาดจาก Backing Array ก้อนใหญ่ ป้องกัน Memory Retention Leak ในระบบ Long-lived CacheFastBytesToString(b []byte) string สำหรับแปลง Network Packet ที่รับจาก Socketunsafe.String และ unsafe.SliceData (Go 1.20+) เพื่อคืนค่าสตริงโดยไม่เกิด Heap Allocation"" โดยปลอดภัย