Go 1.27 ออกแล้ว และของใหม่ที่สำคัญที่สุดคืออะไร

Go 1.27 ออกในเดือนสิงหาคม 2026 ห่างจาก Go 1.26 หกเดือนตามรอบปกติของภาษา การเปลี่ยนแปลงส่วนใหญ่อยู่ที่ toolchain, runtime และ library ไม่ใช่ที่ตัวภาษา และรุ่นนี้ยังอยู่ภายใต้ Go 1 compatibility promise ทีม Go ระบุว่าคาดว่าโปรแกรมเกือบทั้งหมดจะคอมไพล์และทำงานได้เหมือนเดิม

ถ้าอ่านได้แค่สามบรรทัด นี่คือสามเรื่องที่ควรรู้:

  • ภาษารองรับ generic methods แล้ว เมธอดประกาศ type parameter ของตัวเองได้ ซึ่งเป็นข้อจำกัดที่อยู่กับ generics มาตั้งแต่ Go 1.18
  • encoding/json เปลี่ยนเครื่องยนต์ โดยที่ API เดิมไม่เปลี่ยน แพ็กเกจ encoding/json/v2 และ encoding/json/jsontext เข้ามาเป็นของจริงใน standard library และ v1 ทำงานอยู่บน v2 แล้ว
  • post-quantum cryptography เข้ามาอยู่ใน standard library ผ่าน crypto/mldsa พร้อมการรองรับใน crypto/x509 และ crypto/tls

ที่เหลือของบทความคือรายละเอียดของแต่ละเรื่อง และรายการที่ควรตรวจก่อนย้ายระบบที่รันอยู่จริงไปเวอร์ชันนี้

ภาษาเปลี่ยนสามเรื่อง และ generic methods คือเรื่องใหญ่

ตั้งแต่ generics เข้ามาใน Go 1.18 มีข้อจำกัดหนึ่งค้างอยู่: เมธอดประกาศ type parameter ของตัวเองไม่ได้ ถ้าต้องการฟังก์ชันที่รับชนิดอิสระแต่ทำงานกับข้อมูลก้อนหนึ่ง ต้องย้ายออกไปไว้ที่ขอบเขตแพ็กเกจ ผลคือชื่อฟังก์ชันต้องแบกบริบทที่ควรมาจากชนิดข้อมูล และ API แตกเป็นสองที่คือเมธอดชุดหนึ่งกับฟังก์ชันอิสระอีกชุดหนึ่ง

Go 1.27 ปลดข้อจำกัดนี้ และ standard library ใช้ทันทีที่ math/rand/v2 ซึ่งเดิมมีเพียงฟังก์ชัน N ที่ขอบเขตแพ็กเกจ ตอนนี้มีเมธอดที่ประกาศ type parameter ของตัวเองเพิ่มเข้ามา:

// เมธอดประกาศ Int เอง: func (r *Rand) N[Int intType](n Int) Int
r := rand.New(rand.NewPCG(seed1, seed2))

backoff := r.N(30 * time.Second) // Int ถูกอนุมานเป็น time.Duration
shard := r.N(int32(16))          // Int ถูกอนุมานเป็น int32
attempts := r.N(5)               // Int ถูกอนุมานเป็น int

เอกสารของ math/rand/v2 ระบุว่า type parameter Int เป็นชนิดจำนวนเต็มใดก็ได้ ประโยชน์ที่เห็นตรงนี้คือเมธอดเดียวรองรับทุกชนิดจำนวนเต็มโดยไม่ต้องแปลงชนิดไปกลับ และไม่ต้องมีฟังก์ชันคู่ขนานที่ขอบเขตแพ็กเกจอีกชุด

ข้อจำกัดที่ต้องรู้คู่กันอยู่ที่ฝั่ง interface ทั้งคู่:

type Reducer interface {
	// ผิด: เมธอดของ interface ประกาศ type parameter ไม่ได้
	Reduce[R any](f func(int) R) R
}

และเมธอดของ interface ก็ไม่สามารถถูก implement ด้วย generic method ได้ ความหมายเชิงปฏิบัติคือความสามารถนี้อยู่ที่ concrete type ไม่ใช่ที่สัญญาแบบ interface ถ้ากำลังออกแบบ abstraction รอบ interface อย่าเพิ่งวางแผนย้าย helper ทั้งหมดเข้าไปเป็นเมธอด เพราะสิ่งที่ interface รับได้ยังเท่าเดิม

อีกสองเรื่องที่เปลี่ยนในภาษาเป็นการผ่อนข้อจำกัดเดิม:

  • คีย์ใน struct literal เป็น field selector ที่ถูกต้องอันใดก็ได้ ไม่จำกัดแค่ชื่อฟิลด์ระดับบนสุดของ struct นั้น
  • การอนุมานชนิดของฟังก์ชันถูกขยายให้ครอบคลุมทุกบริบท ที่ฟังก์ชัน generic ถูกกำหนดค่าให้ตัวแปร หรือถูกแปลงไปเป็นชนิดฟังก์ชันที่เข้ากันได้ ผลคือกรณีที่เดิมต้องระบุ type argument เองหลายจุดจะเขียนสั้นลง

ทั้งสามเรื่องเป็นการเพิ่มความสามารถ ไม่ใช่การถอด โค้ดที่เขียนไว้แล้วจึงไม่ได้รับผลกระทบจากหัวข้อนี้

encoding/json/v2 คือการเปลี่ยนแปลงที่กระทบโค้ดเดิมมากที่สุด

Go 1.27 เพิ่มสองแพ็กเกจเข้ามาพร้อมกัน และเปลี่ยนสิ่งที่อยู่ข้างใต้แพ็กเกจที่แทบทุกโปรแกรมใช้อยู่

encoding/json หลัง Go 1.27: ผิวเดิม เครื่องยนต์ใหม่

สามแพ็กเกจเรียงจาก API ที่โค้ดเดิมเรียกอยู่ ลงไปถึงชั้นที่อ่านและเขียนไวยากรณ์ JSON โดยตรง

โค้ดเดิมของคุณเรียกที่นี่

  1. encoding/jsonAPI เดิม
    ชั้น API v1 ที่โปรแกรมส่วนใหญ่ใช้อยู่

    ยังเป็น API ชุดเดิมและยังได้รับการดูแลต่อ ไม่ต้องย้ายโค้ด แต่ภายในเปลี่ยนไปเรียก v2 แล้ว พฤติกรรม marshal และ unmarshal ยังเหมือนเดิม สิ่งที่อาจต่างคือข้อความใน error

  2. encoding/json/v2ใหม่ใน 1.27
    ชั้นแปลงค่าระหว่าง Go กับ JSON

    มี Marshal, MarshalWrite, MarshalEncode, Unmarshal, UnmarshalRead และ UnmarshalDecode ทุกตัวรับ Options แบบ variadic ค่าเริ่มต้นเข้มกว่า v1 คือปฏิเสธ UTF-8 ที่ไม่ถูกต้อง และปฏิเสธชื่อฟิลด์ซ้ำในออบเจ็กต์เดียวกัน

  3. encoding/json/jsontextใหม่ใน 1.27
    ชั้นไวยากรณ์ JSON ล้วน

    Encoder และ Decoder ทำงานกับ JSON ในรูป Token และ Value โดยมี state machine คุมให้ลำดับที่อ่านหรือเขียนออกมาเป็น JSON ที่ถูกต้องเสมอ ใช้เมื่อต้องประมวลผลสตรีมโดยไม่ผูกกับ struct

v1 เรียกลงไปที่ v2 · v2 เรียกลงไปที่ jsontext

ทางถอย

หากเจอปัญหาความเข้ากันได้ ตั้ง GOEXPERIMENT=nojsonv2 ตอน build เพื่อกลับไปใช้ implementation v1 เดิม ทางถอยนี้ทีม Go ระบุว่าจะถูกถอดออกในรุ่นถัดไป จึงควรใช้เพื่อซื้อเวลา ไม่ใช่เป็นค่าถาวร

เรื่องแรกที่ต้องรู้เป็นเรื่องเชิงปฏิบัติล้วน ๆ: แพ็กเกจ encoding/json/v2 ใช้ชื่อแพ็กเกจว่า json เหมือน v1 ถ้าไฟล์เดียวต้องใช้ทั้งสองตัว ต้องตั้ง alias ให้ตัวใดตัวหนึ่ง

import (
	jsonv1 "encoding/json"   // API เดิม ต้อง alias เพราะชื่อแพ็กเกจชนกัน
	"encoding/json/v2"       // ชื่อแพ็กเกจคือ json
	"encoding/json/jsontext" // ชั้นไวยากรณ์
)

v2 ให้ Marshal, MarshalWrite, MarshalEncode, Unmarshal, UnmarshalRead และ UnmarshalDecode ทุกตัวรับ Options แบบ variadic ซึ่งเป็นความต่างเชิงออกแบบที่สำคัญที่สุดจาก v1: เดิมพฤติกรรมถูกกำหนดด้วย struct tag เป็นหลัก ตอนนี้กำหนดต่อการเรียกได้

type Config struct {
	Name    string `json:"name"`
	Workers int    `json:"workers"`
}

// Deterministic ทำให้ลำดับคีย์ของ map คงที่ ใช้ตอนต้องเทียบผลลัพธ์หรือทำ checksum
b, err := json.Marshal(cfg, json.Deterministic(true))
if err != nil {
	return err
}

// ความเข้มงวดสั่งได้ต่อการเรียก ไม่ต้องประกาศไว้ที่ชนิดข้อมูล
var got Config
if err := json.Unmarshal(b, &got, json.RejectUnknownMembers(true)); err != nil {
	return err
}

// อ่านจาก io.Reader ได้ตรง ๆ โดยไม่ต้องผ่าน []byte กลาง
if err := json.UnmarshalRead(resp.Body, &got); err != nil {
	return err
}

ค่าเริ่มต้นของ v2 เข้มกว่าและเข้ากันกับระบบอื่นได้ดีกว่า v1 สองข้อหลักคือปฏิเสธ UTF-8 ที่ไม่ถูกต้องในสตริง JSON และปฏิเสธชื่อฟิลด์ซ้ำภายในออบเจ็กต์เดียวกัน ข้อหลังคือจุดที่ควรสังเกตเป็นพิเศษ เพราะ payload ที่มีคีย์ซ้ำเคยผ่าน v1 ไปเงียบ ๆ โดยใช้ค่าสุดท้าย

encoding/json/jsontext เป็นชั้นที่อยู่ต่ำลงไป จัดการ JSON ในเชิงไวยากรณ์ล้วน Encoder และ Decoder ทำงานกับ JSON ในรูป Token และ Value โดยมี state machine คุมให้ลำดับที่ผลิตหรือบริโภคออกมาเป็น JSON ที่ถูกต้องเสมอ เหมาะกับงานสตรีมที่ไม่ควรผูกกับ struct เช่นการสแกนหาฟิลด์เดียวจากเอกสารขนาดใหญ่โดยไม่ต้อง decode ทั้งก้อน:

dec := jsontext.NewDecoder(resp.Body)
for {
	tok, err := dec.ReadToken()
	if errors.Is(err, io.EOF) {
		break
	}
	if err != nil {
		return err
	}
	if tok.Kind() == jsontext.KindString {
		// จัดการเฉพาะโทเคนที่เป็นสตริง
	}
}

Kind เป็นชนิด byte ที่ค่าตรงกับอักขระตัวแรกของสัญลักษณ์ในไวยากรณ์ JSON จึงมีค่าคงที่อย่าง KindString, KindNumber, KindBeginObject และ KindEndArray ให้ใช้แทนการเทียบกับอักขระดิบ

เรื่องที่กระทบทุกคนคือ encoding/json ตอนนี้ทำงานอยู่บน implementation ของ v2 ทีม Go ระบุว่าพฤติกรรมของ marshal และ unmarshal ถูกรักษาไว้ แต่ข้อความใน error อาจต่างจากเดิม และแพ็กเกจ v1 ได้ Options เพิ่มขึ้นหลายตัวสำหรับสั่งให้ v2 ทำงานด้วยความหมายแบบ v1 เพื่อไม่ให้ต้องย้าย API ทั้งหมด ทีม Go ยืนยันว่า API v1 จะยังได้รับการดูแลต่อ และผู้ใช้ไม่จำเป็นต้องย้าย

ด้านประสิทธิภาพ ทีม Go ระบุว่า marshal อยู่ในระดับใกล้เคียงกับ implementation เดิม ส่วน unmarshal เร็วขึ้นอย่างมีนัยสำคัญ

ระหว่างที่ v2 ยังเป็น GOEXPERIMENT มีการถอดและเปลี่ยนหลายอย่างที่ควรรู้ก่อนเขียนโค้ดใหม่บน v2:

  • ถอด tag option ชื่อ format และ unknown
  • ถอด marshal option ชื่อ DiscardUnknownMembers และ sentinel error ชื่อ SkipFunc
  • เปลี่ยนชื่อ tag option inline เป็น embed
  • ปรับพฤติกรรมของ tag option string และของ option ชื่อ MatchCaseInsensitiveNames
  • ใน jsontext ตัวอ่านค่าเชิงตัวเลขของ Token เปลี่ยนไปคืน error ด้วย

หากอัปเกรดแล้วเจอปัญหาความเข้ากันได้ ตั้ง GOEXPERIMENT=nojsonv2 ตอน build เพื่อกลับไปใช้ implementation v1 เดิม แต่ทีม Go ระบุว่าทางถอยนี้จะถูกถอดออกในรุ่นถัดไป จึงควรใช้เพื่อซื้อเวลาแก้ปัญหา ไม่ใช่ตั้งค่าถาวร

Runtime: จัดสรรหน่วยความจำเร็วขึ้น และหา goroutine leak ได้ในระบบจริง

compiler เรียก routine จัดสรรหน่วยความจำที่แยกตามขนาดแล้ว ผลคือลดต้นทุนการจัดสรรก้อนเล็กบางกรณีที่ขนาดต่ำกว่า 80 ไบต์ได้ถึง 30% ทีม Go ระบุว่าผลลัพธ์ต่างกันตาม workload และคาดว่าโปรแกรมที่จัดสรรหน่วยความจำหนักจะดีขึ้นราว 1% โดยรวม ต้นทุนที่แลกมาคือไบนารีใหญ่ขึ้นราว 60 KB โดยไม่ขึ้นกับ workload ปิดได้ด้วย GOEXPERIMENT=nosizespecializedmalloc ตอน build และทีม Go คาดว่าจะถอดตัวเลือกนี้ออกใน Go 1.28

ตัวเลข 30% เป็นการลดต้นทุนของการจัดสรรเอง ไม่ใช่ของโปรแกรมทั้งโปรแกรม ตัวเลขที่ควรคาดหวังจริงคือราว 1% และเฉพาะกับโปรแกรมที่จัดสรรหนักเท่านั้น

เรื่องที่สองมีค่ากับทีมที่ดูแลระบบระยะยาวมากกว่า: profile สำหรับ goroutine leak ใช้งานได้ทั่วไปแล้ว หลังจากเป็นการทดลองใน Go 1.26 ชื่อ profile คือ goroutineleak ดึงผ่าน runtime/pprof ได้เหมือน profile อื่น:

f, err := os.Create("goroutineleak.pprof")
if err != nil {
	return err
}
defer f.Close()

if p := pprof.Lookup("goroutineleak"); p != nil {
	if err := p.WriteTo(f, 0); err != nil {
		return err
	}
}

ถ้าโปรแกรม import net/http/pprof อยู่แล้ว จะมี endpoint ให้ดึงตรงได้เลย:

go tool pprof http://localhost:6060/debug/pprof/goroutineleak

นิยามที่ runtime ใช้ชัดเจนและแคบกว่าที่หลายคนเข้าใจ: goroutine ที่รั่วคือ goroutine ที่ถูกบล็อกอยู่กับ concurrency primitive เช่น channel, sync.Mutex หรือ sync.Cond แล้วไม่มีทางถูกปลดบล็อกได้อีก runtime ตรวจด้วย garbage collector คือถ้า goroutine G ถูกบล็อกอยู่กับ primitive P และ P เข้าถึงไม่ได้จาก goroutine ที่รันได้ หรือจาก goroutine ที่ตัวมันปลดบล็อกต่อได้ แปลว่า P ไม่มีทางถูกปลด goroutine G จึงไม่มีทางตื่น

ข้อจำกัดมาจากวิธีตรวจโดยตรง เพราะเทคนิคนี้ตั้งอยู่บนการเข้าถึงได้ runtime จึงอาจไม่พบการรั่วที่เกิดจากการบล็อกกับ primitive ที่ยังเข้าถึงได้ผ่านตัวแปร global หรือผ่านตัวแปรภายในของ goroutine ที่ยังรันอยู่ ผลลัพธ์ที่ว่างจึงไม่ใช่หลักฐานว่าไม่มีการรั่ว ต่างจาก profile goroutine แบบเดิมที่บอกได้ว่ามี goroutine ค้างอยู่เท่าไร แต่ไม่บอกว่าตัวไหนไม่มีทางตื่น profile ใหม่ตอบคำถามหลังนี้ได้ งานนี้ทีม Go ให้เครดิต Vlad Saioc จาก Uber

อีกสองเรื่องใน runtime ที่กระทบพฤติกรรมโดยตรง:

  • traceback มี label ของ goroutine ติดมาด้วย สำหรับโมดูลที่ระบุ go 1.27 ขึ้นไป โดยดึงจาก label ของ runtime/pprof มาใส่ในบรรทัดหัวของ traceback ปิดได้ด้วย GODEBUG ชื่อ tracebacklabels=0 ซึ่งทีม Go ระบุว่าตั้งใจเก็บทางปิดนี้ไว้ตลอดไป เผื่อกรณีที่ label มีข้อมูลอ่อนไหวที่ไม่ควรโผล่ใน traceback
  • GODEBUG ชื่อ asynctimerchan ถูกถอดถาวร channel ที่แพ็กเกจ time สร้างจะเป็นแบบ unbuffered เสมอ ไม่ว่าจะตั้ง GODEBUG อย่างไร

Cryptography: post-quantum เข้ามาอยู่ใน standard library

แพ็กเกจใหม่ crypto/mldsa implement ระบบลายเซ็นแบบ post-quantum ชื่อ ML-DSA ตามที่กำหนดใน FIPS 204 API เดินตามรูปแบบเดียวกับแพ็กเกจ crypto อื่นในไลบรารีมาตรฐาน:

sk, err := mldsa.GenerateKey(mldsa.MLDSA65())
if err != nil {
	return err
}

// Context แยกลายเซ็นที่สร้างคนละวัตถุประสงค์ออกจากกัน ยาวได้ไม่เกิน 255 ไบต์
opts := &mldsa.Options{Context: "release-manifest"}

sig, err := sk.Sign(nil, manifest, opts)
if err != nil {
	return err
}

if err := mldsa.Verify(sk.PublicKey(), manifest, sig, opts); err != nil {
	return err
}

จุดที่พลาดกันได้คือ Context ต้องเป็นค่าเดียวกันทั้งตอนเซ็นและตอนตรวจ ถ้าใส่ไม่ตรงกันการตรวจจะไม่ผ่าน และแพ็กเกจนี้ให้ทั้ง Sign ที่ใช้ randomness กับ SignDeterministic สำหรับกรณีที่ต้องการผลลัพธ์เดิมทุกครั้ง

การรองรับเดินต่อขึ้นไปทั้งสาย:

  • crypto/x509 รองรับ private key, public key และลายเซ็นแบบ ML-DSA แล้ว
  • crypto/tls รองรับลายเซ็น ML-DSA ใน TLS 1.3 ผ่านค่า SignatureScheme ใหม่สามค่าคือ MLDSA44, MLDSA65 และ MLDSA87
  • crypto เพิ่มค่า Hash ชื่อ MLDSAMu ไว้เป็นกลไกส่งสัญญาณสำหรับการเซ็นแบบ External μ ML-DSA

ฝั่งการแลกกุญแจ crypto/tls รองรับ MLKEM1024 แล้ว เปิดใช้ได้โดยเพิ่มเข้าไปใน Config.CurvePreferences และ Go 1.27 แก้ความสับสนเดิมด้วยว่า hybrid key exchange แบบ post-quantum เปิดใช้อย่างชัดเจนใน Config.CurvePreferences ได้ แม้จะตั้ง GODEBUG tlsmlkem=0 หรือ tlssecpmlkem=0 ไว้ เพราะสองตัวนั้นตั้งใจให้มีผลกับชุดค่าเริ่มต้นที่ใช้เมื่อ Config.CurvePreferences เป็น nil เท่านั้น

การเปลี่ยนแปลงอื่นใน crypto/tls และ crypto/x509 ที่ควรรู้:

  • Config.Rand ถูกประกาศเป็น deprecated แล้ว สำหรับการทดสอบแบบกำหนดผลได้ให้ใช้ testing/cryptotest.SetGlobalRandom แทน
  • ฟิลด์ใหม่ ConnectionState.LocalCertificate เก็บสายใบรับรองที่ฝั่งเราแสดงต่อคู่สนทนาตอน handshake
  • GODEBUG ห้าตัวถูกถอดถาวรคือ tlsunsafeekm, tlsrsakex, tls3des, tls10server และ x509keypairleaf ระบบที่ยังพึ่งค่าเก่าเหล่านี้อยู่จะไม่มีทางกลับ
  • SystemCertPool เคารพตัวแปรสภาพแวดล้อม SSL_CERT_FILE และ SSL_CERT_DIR บน Windows และ Darwin แล้ว โดยจะใช้ verifier ฝั่ง Go เมื่อมีการตั้งตัวแปรเหล่านี้ ปิดได้ด้วย GODEBUG=x509sslcertoverrideplatform=0

ข้อสุดท้ายมีผลกับ container และ CI ที่ตั้งตัวแปรสองตัวนี้ไว้อยู่แล้วโดยไม่ได้คาดว่าจะมีผลกับโปรแกรม Go ควรตรวจว่าการยืนยันใบรับรองยังให้ผลเหมือนเดิมหลังอัปเกรด สำหรับระบบที่เชื่อมต่อกันหลายฝั่งและต้องคุมขอบเขตความน่าเชื่อถือ อ่านต่อได้ที่ แนวทางเชื่อม AI กับ ERP, CRM และระบบภายในให้ปลอดภัย

net/http: การเปลี่ยนแปลงที่กระทบเซิร์ฟเวอร์ที่รันอยู่

หัวข้อนี้เปลี่ยนพฤติกรรมของโค้ดที่ทำงานอยู่แล้ว ไม่ใช่แค่เพิ่ม API เริ่มจากสองฟิลด์ที่ตั้งได้ทันที:

srv := &http.Server{
	Addr:    ":8080",
	Handler: mux,

	// จำกัดจำนวนค่าใน header ที่ยอมรับ ถ้าไม่ตั้งจะใช้
	// http.DefaultMaxHeaderValueCount ซึ่งมีค่าเท่ากับ 500
	MaxHeaderValueCount: 1000,

	// ตั้งเป็น true เพื่อกลับไปเสิร์ฟ stream แบบ round-robin
	// โดยไม่สนสัญญาณลำดับความสำคัญจาก client
	DisableClientPriority: false,
}
  • Response.Body ของ HTTP/1 ระบายเนื้อหาที่ยังไม่ได้อ่านทิ้งตอนถูก close ภายในขีดจำกัดที่ตั้งไว้อย่างระมัดระวัง เพื่อให้ reuse connection ได้ดีขึ้น ทีม Go ระบุว่าสำหรับโปรแกรมส่วนใหญ่การเปลี่ยนนี้ไม่มีผล หรือทำให้เร็วขึ้น แต่ในบางกรณีที่ไม่ได้ประโยชน์จากการ reuse connection และปล่อยให้มี idle connection ค้างมากเกินไป เช่น ตั้ง Transport.MaxIdleConns เป็น 0 หรือใช้ Client คนละตัวต่อคำขอจนเลี่ยงขีดจำกัดนั้นไป อาจเห็นประสิทธิภาพแย่ลง ทางแก้เฉพาะหน้าคือตั้ง Transport.DisableKeepAlives เป็น true แต่ทีม Go ตั้งข้อสังเกตว่าอาการแบบนี้มักแปลว่า Transport หรือ Client ถูกตั้งค่าหรือใช้งานผิดตั้งแต่ต้น
  • เซิร์ฟเวอร์ HTTP/2 รับสัญญาณลำดับความสำคัญจาก client ตาม RFC 9218 แล้ว และจะจัดลำดับการเสิร์ฟ stream ตามนั้น
  • Transport และ Server รองรับการเจรจา ALPN บน net.Conn ที่ผู้ใช้ส่งเข้ามาเอง ถ้า connection นั้นมีเมธอด ConnectionState() tls.ConnectionState

ฝั่งการทดสอบมีของใหม่สองตัวที่ใช้คู่กันได้ดี net/http/httptest เพิ่ม NewTestServer ที่สร้าง Server บนเครือข่ายจำลองในหน่วยความจำ ออกแบบมาให้ใช้กับ testing/synctest และ testing/synctest เพิ่ม Sleep ที่รวม time.Sleep กับ synctest.Wait เข้าด้วยกัน ผลคือเทสต์ที่มีทั้งเวลาและ HTTP เข้ามาเกี่ยวข้องเขียนให้กำหนดผลได้จริง โดยไม่ต้องรอเวลาจริง:

func TestRetryBackoff(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		srv := httptest.NewTestServer(t, http.HandlerFunc(
			func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusServiceUnavailable)
			},
		))

		go pollForever(srv.Client(), srv.URL)

		// เดินนาฬิกาจำลองไป 5 วินาที แล้วรอให้ทุก goroutine นิ่ง
		synctest.Sleep(5 * time.Second)

		// ตรวจได้ว่าใน 5 วินาทีนั้น poller ยิงไปกี่ครั้ง
	})
}

NewTestServer รับ testing.TB เป็นอาร์กิวเมนต์แรก จึงปิดเซิร์ฟเวอร์ให้เองเมื่อเทสต์จบ ไม่ต้องเขียน defer srv.Close()

เครื่องมือ: go test, go doc, go fix และ go mod tidy

go test เรียก vet check ชื่อ stdversion เป็นค่าเริ่มต้นแล้ว ตัวตรวจนี้รายงานการใช้สัญลักษณ์ของ standard library ที่ใหม่เกินกว่าเวอร์ชัน Go ที่มีผลกับไฟล์นั้น โดยดูจาก go directive ใน go.mod และ build tag บนไฟล์ ผลคือโค้ดที่เผลอใช้ API ใหม่ทั้งที่ประกาศรองรับเวอร์ชันเก่าจะถูกจับตั้งแต่ตอนเทสต์ แทนที่จะไปพังที่เครื่องผู้ใช้

go test -json เพิ่มฟิลด์ "OutputType" บนบรรทัดที่ "Action" เป็น "output" เพื่อบอกชนิดของ output ค่าที่เป็นไปได้ตอนนี้รวมถึง "error", "error-continue" และ "frame" โครงสร้างด้านล่างเป็นตัวอย่างเพื่อให้เห็นตำแหน่งของฟิลด์ใหม่ ไม่ใช่ผลลัพธ์จริงจากการรัน:

{
  "Action": "output",
  "Package": "example.com/app",
  "Test": "TestUpgrade",
  "Output": "    main_test.go:42: unexpected status 500\n",
  "OutputType": "error"
}

ทีมที่แปลผลเทสต์ใน CI เองได้ประโยชน์จากฟิลด์นี้มากที่สุด เพราะแยกบรรทัดที่เป็น error ออกจาก output ทั่วไปได้โดยไม่ต้องเดาจากรูปแบบข้อความ

คำสั่งอื่นที่เปลี่ยน:

go doc example.com/pkg@v1.2.3
go doc -ex bytes.Buffer
go fix ./...
go mod tidy
  • go doc รองรับรูปแบบ package@version แล้ว และรับตัวเลือก -ex เพื่อลิสต์ตัวอย่างที่รันได้ของแพ็กเกจหรือสัญลักษณ์ที่ระบุ ถ้าส่งชื่อตัวอย่างเข้าไปตรง ๆ จะพิมพ์ซอร์สของตัวอย่างพร้อมคอมเมนต์ออกมาด้วย
  • go fix เพิ่ม modernizer สี่ตัวคือ atomictypes, embedlit, slicesbackward และ unsafefuncs ถอด analyzer ชื่อ fmtappendf ออกด้วยเหตุผลด้านสไตล์ และเปลี่ยนชื่อ analyzer waitgroup เป็น waitgroupgo เพื่อลดความกำกวม
  • go mod tidy รวม require block ที่ซ้ำกันให้อัตโนมัติ สำหรับโมดูลที่ระบุ go 1.27 ขึ้นไป ผลคือ go.mod เหลือ require block มากที่สุดสองก้อนคือ direct และ indirect โดยคอมเมนต์ที่ผูกกับ dependency เดิมจะถูกเก็บไว้ ปัญหาที่แก้คือ go.mod ที่สะสม require block กระจัดกระจายจากการแก้มือ การ merge ที่ค้าง หรือการอัปเกรดยุคเก่า
  • go tool trace จำกัด address ที่ฟังไว้ที่ localhost เมื่อส่งเฉพาะพอร์ตให้ -http เช่น -http=:6060 ให้พฤติกรรมตรงกับ -http ของ go tool pprof ถ้าต้องการฟังทุก address ต้องระบุเอง เช่น -http=0.0.0.0:6060
  • go command ไม่รองรับระบบ version control ชื่อ bzr อีกต่อไป โมดูลที่โฮสต์บนเซิร์ฟเวอร์ bzr จะดึงตรง ๆ ไม่ได้แล้ว

นอกจากนี้ tool ชื่อ compile, link, asm, cgo, cover และ pack รองรับ response file แบบ @file แล้ว รูปแบบเข้ากันได้กับ response file ของ GCC เพื่อให้ระบบ build ที่มีอยู่เดิมทำงานร่วมกันได้

ของเล็กที่หยิบใช้ได้ทันที

strings.CutLast และ bytes.CutLast เป็นตัวอย่างที่เห็นผลชัดที่สุด ทั้งคู่ตัดรอบตัวคั่น ตัวสุดท้าย และมีลายเซ็นเหมือน Cut ที่มีอยู่แล้วทุกประการ:

// เดิม: LastIndex บวกการตัดสองครั้ง และต้องระวังขอบเขตเอง
host, port := addr, ""
if i := strings.LastIndex(addr, ":"); i >= 0 {
	host, port = addr[:i], addr[i+1:]
}

// Go 1.27: func CutLast(s, sep string) (before, after string, found bool)
host, port, ok := strings.CutLast(addr, ":")
if !ok {
	// ไม่มีตัวคั่น: host คือ addr ทั้งก้อน และ port เป็นสตริงว่าง
}

รายการที่เหลือไม่เปลี่ยนสถาปัตยกรรมของใคร แต่ลดโค้ดน่ารำคาญได้จริง:

  • net/url เพิ่มเมธอด URL.Clone และ Values.Clone สำหรับก๊อบปี้แบบลึก
  • math/big เพิ่มเมธอด Int.Divide ที่คืนทั้งผลหารและเศษในครั้งเดียว ลายเซ็นคือ func (z *Int) Divide(x, y, r *Int, mode RoundingMode) (*Int, *Int) และรุ่นนี้เพิ่มค่าคงที่โหมดปัดสี่ตัวคือ Trunc, Floor, Round และ Ceil
  • database/sql เพิ่มฟังก์ชัน ConvertAssign ให้ driver เข้าถึงการแปลงชนิดชุดเดียวกับที่ Rows.Scan ใช้ และ database/sql/driver เพิ่ม interface ชื่อ RowsColumnScanner ที่มีเมธอด NextRow กับ ScanColumn สำหรับ scan ลงปลายทางที่ผู้ใช้กำหนดโดยตรง
  • แพ็กเกจใหม่ uuid สร้างและ parse UUID ได้ในตัว มี uuid.New() สำหรับรุ่น 4 แบบสุ่ม uuid.NewV7() สำหรับรุ่น 7 ที่เรียงตามเวลา และ uuid.Parse(s string) (UUID, error)
  • hash/maphash เพิ่ม interface ชื่อ Hasher และชนิด ComparableHasher ส่วน go/types เพิ่มชนิด Hasher และ HasherIgnoreTags ทำให้ใช้ Type เป็นคีย์ใน hash table ได้
  • unicode และส่วนรองรับที่เกี่ยวข้องทั้งระบบขยับจาก Unicode 15 ไป Unicode 17
  • net เปลี่ยนให้เมธอดอ่านของ UnixConn คืน io.EOF ตรง ๆ ไม่ห่อด้วย net.OpError อีก

uuid.NewV7() น่าสนใจเป็นพิเศษสำหรับงานฐานข้อมูล เพราะ UUID รุ่น 7 มี timestamp นำหน้า ค่าที่สร้างต่อเนื่องกันจึงเรียงใกล้กัน ต่างจากรุ่น 4 ที่สุ่มทั้งก้อนและทำให้ index กระจาย

สำหรับงานที่ต้องการ SIMD Go 1.27 เพิ่มแพ็กเกจทดลองชื่อ simd ที่ให้ vector type แบบไม่ผูกกับขนาด เช่น Int8s และ Float32s ใช้ได้บนทุกสถาปัตยกรรม ส่วน simd/archsimd ที่เริ่มใน Go 1.26 เพิ่มการรองรับ arm64 Neon แบบ 128 บิต และ WebAssembly แบบ 128 บิต ทั้งสองแพ็กเกจเปิดด้วย GOEXPERIMENT=simd ตอน build และ API ยังไม่นิ่ง จึงยังไม่ควรผูกระบบ production ไว้กับมัน

ตรวจห้าด่านนี้ก่อนย้าย production

ลำดับการตรวจก่อนย้าย production ไป Go 1.27

ห้าด่านเรียงจากสิ่งที่พังเสียงดังไปหาสิ่งที่พังเงียบ แต่ละด่านมีสิ่งที่ต้องตรวจ และทางออกเมื่อไม่ผ่าน

  1. build และ vet
    • go test เรียก vet check ชื่อ stdversion เป็นค่าเริ่มต้นแล้ว จะรายงานการใช้สัญลักษณ์ของ standard library ที่ใหม่เกินกว่าเวอร์ชันที่ระบุใน go directive
    • go command จะ fail ถ้า go.mod หรือ //go:debug ตั้ง GODEBUG ที่ถูกถอดออกไปแล้วไว้ที่ค่าเก่า
    • โมดูลที่โฮสต์บนเซิร์ฟเวอร์ bzr ดึงไม่ได้อีกต่อไป
    • เครื่อง build และเครื่องรันบน macOS ต้องเป็น macOS 13 Ventura ขึ้นไป

    ถ้าไม่ผ่านปรับ go directive ให้ตรงกับของจริง และตั้ง GODEBUG ที่ถูกถอดออกไว้ที่ค่าเริ่มต้นสุดท้ายก่อนถูกถอด ซึ่ง go command ยังรับอยู่

  2. ผลลัพธ์ที่เทียบทีละไบต์
    • compress/flate เร็วขึ้น แต่ไบต์ที่ Writer เขียนออกมาอาจต่างจาก Go 1.26
    • DEFLATE เป็นตัวบีบอัดที่อยู่ใต้ archive/zip, compress/gzip, compress/zlib และ image/png ผลลัพธ์ของทั้งสี่แพ็กเกจจึงอาจเปลี่ยนตาม
    • golden file, checksum และลายเซ็นที่คำนวณจากไบต์หลังบีบอัด คือจุดที่จะแตกก่อน

    ถ้าไม่ผ่านด่านนี้ไม่มีสวิตช์ให้ปิด เปลี่ยนไปเทียบข้อมูลหลังคลายบีบอัดแทนการเทียบไบต์ที่บีบอัดแล้ว

  3. พฤติกรรมตอนรัน
    • GODEBUG ชื่อ asynctimerchan ถูกถอดถาวร channel ที่แพ็กเกจ time สร้างจะเป็นแบบ unbuffered เสมอ
    • Response.Body ของ HTTP/1 จะระบายเนื้อหาที่ยังไม่ได้อ่านทิ้งตอนถูก close ภายในขีดจำกัดที่ตั้งไว้อย่างระมัดระวัง เพื่อให้ reuse connection ได้ดีขึ้น
    • เซิร์ฟเวอร์ HTTP/2 รับสัญญาณลำดับความสำคัญจาก client ตาม RFC 9218 แล้ว
    • compiler ตั้งชื่อ function literal ให้สั้นและคงที่ขึ้น เทสต์ที่ยืนยันชื่อสัญลักษณ์อาจต้องแก้ และโค้ดที่เทียบ code pointer ของฟังก์ชันว่าเท่ากันจะเห็นปัญหาชัดขึ้น

    ถ้าไม่ผ่านตั้ง Server.DisableClientPriority เป็น true เพื่อกลับไปเสิร์ฟแบบ round-robin และตั้ง Transport.DisableKeepAlives เป็น true เพื่อปิดการ reuse connection

  4. JSON
    • encoding/json ยังเป็น API ชุดเดิม แต่ตอนนี้ทำงานบน implementation ของ v2
    • พฤติกรรม marshal และ unmarshal ถูกรักษาไว้ แต่ข้อความใน error อาจต่างจากเดิม
    • เทสต์หรือ log parser ที่จับคู่กับข้อความ error แบบตรงตัว คือจุดที่จะแตก

    ถ้าไม่ผ่านตั้ง GOEXPERIMENT=nojsonv2 ตอน build เพื่อกลับไปใช้ implementation v1 โดยทีม Go ระบุว่าทางถอยนี้จะถูกถอดออกในรุ่นถัดไป

  5. วัดผลหลังอัปเกรด
    • compiler เรียก routine จัดสรรหน่วยความจำที่แยกตามขนาดแล้ว ลดต้นทุนการจัดสรรก้อนเล็กกว่า 80 ไบต์ได้ถึง 30% และคาดว่าโปรแกรมที่จัดสรรหนักจะดีขึ้นราว 1% โดยรวม
    • ไบนารีใหญ่ขึ้นราว 60 KB โดยไม่ขึ้นกับ workload
    • profile ชื่อ goroutineleak ใช้งานได้ทั่วไปแล้วใน runtime/pprof และที่ endpoint /debug/pprof/goroutineleak

    ถ้าไม่ผ่านตั้ง GOEXPERIMENT=nosizespecializedmalloc ตอน build เพื่อปิดการจัดสรรแบบแยกตามขนาด โดยทีม Go คาดว่าจะถอดตัวเลือกนี้ออกใน Go 1.28

สิ่งที่ไม่เปลี่ยน

Go 1.27 ยังอยู่ภายใต้ Go 1 compatibility promise ทีม Go คาดว่าโปรแกรมเกือบทั้งหมดจะคอมไพล์และทำงานได้เหมือนเดิม ห้าด่านนี้คือรายการที่ควรตรวจ ไม่ใช่รายการที่คาดว่าจะพัง

สองเรื่องที่อยู่นอกโค้ดแต่ตัดสินได้ว่าอัปเกรดได้หรือไม่คือแพลตฟอร์ม:

  • macOS ต้องเป็นเวอร์ชัน 13 Ventura ขึ้นไป ตามที่ประกาศไว้ในบันทึกรุ่นของ Go 1.26 การรองรับเวอร์ชันก่อนหน้าถูกยุติแล้ว ทั้งเครื่องนักพัฒนาและ runner ของ CI ที่ยังใช้ macOS เก่ากว่านั้นต้องขยับก่อน
  • PowerPC แบบ big-endian 64 บิตบน Linux คือ GOOS=linux กับ GOARCH=ppc64 เปลี่ยนไปสร้างไบนารีที่ใช้ ABI ระบบแบบ ELFv2 ซึ่งต้องการ Linux kernel 3.13 ขึ้นไป โดย RHEL7 backport การรองรับนี้ลงไปที่ kernel 3.10 ของตัวเองแล้ว ผลดีคือรองรับ cgo, position-independent executable และ external linking ได้แล้ว แต่ต้องมี runtime ที่เข้ากันได้กับ ELFv2 คือทั้ง libc และไลบรารีทุกตัวที่ถูกลิงก์หรือโหลด สำหรับโปรแกรมที่ไม่ใช้ cgo toolchain ยังสร้างไบนารีแบบ static ด้วย internal linking ให้เป็นค่าเริ่มต้น ส่วนโปรแกรมที่มีตัวเลือก cgo หากต้องการไบนารี static ที่เป็น Go ล้วนให้ตั้ง CGO_ENABLED=0 ตอนรัน go build

เมื่อผ่านทั้งห้าด่านแล้ว วิธีตรวจว่าทางถอยแต่ละตัวยังทำงานคือ build ด้วยตัวเลือกนั้นแล้วรันชุดเทสต์เดิมซ้ำ:

GOEXPERIMENT=nojsonv2 go build ./...
GOEXPERIMENT=nosizespecializedmalloc go build ./...

สรุป: อัปเกรดได้ แต่ให้เวลากับสามจุด

Go 1.27 เป็นรุ่นที่เพิ่มความสามารถมากกว่าถอดของ และยังอยู่ภายใต้ Go 1 compatibility promise สำหรับโปรเจกต์ส่วนใหญ่การอัปเกรดจะเป็นเรื่องของการรันเทสต์แล้วผ่าน

สามจุดที่ควรให้เวลาตรวจจริงคือ ข้อความ error จาก encoding/json ที่อาจเปลี่ยนเพราะ v1 ทำงานบน v2 แล้ว ไบต์ที่ออกจาก compress/flate และแพ็กเกจที่ใช้ DEFLATE ต่อ ซึ่งอาจต่างจาก Go 1.26 และพฤติกรรมของ net/http ที่ระบาย Response.Body ตอน close พร้อมกับการจัดลำดับ stream ของ HTTP/2 ทั้งสามจุดมีทางถอยหรือมีวิธีแก้ที่ชัดเจน แต่ไม่มีจุดไหนที่จะประกาศตัวเองถ้าไม่ได้ตั้งใจตรวจ

หลังอัปเกรดแล้ว ของใหม่ที่ให้ผลตอบแทนเร็วที่สุดคือ profile goroutineleak เพราะเปลี่ยนการหา goroutine ที่ค้างจากการเดาให้เป็นการวัด และไม่ต้องแก้โค้ดใด ๆ เพื่อเริ่มใช้

หากต้องการให้ทีมช่วยประเมินแผนอัปเกรดหรือออกแบบ backend ที่ดูแลระยะยาวได้ อ่านรายละเอียด บริการพัฒนาซอฟต์แวร์และระบบเฉพาะทาง หรือ ติดต่อ Nixxel เพื่อเริ่มวิเคราะห์โจทย์

แหล่งอ้างอิงและอ่านต่อ