Go 1.22 เปิดตัวแล้ว
(go.dev)- ตัวแปรของลูป
forถูกเปลี่ยนให้สร้างใหม่ในแต่ละรอบ เพื่อหลีกเลี่ยงบั๊กจากการแชร์โดยไม่ตั้งใจ และสามารถใช้rangeกับจำนวนเต็มได้แล้ว - ความเข้ากันได้ ยังคงรักษาคำมั่นสัญญาเรื่องความเข้ากันได้ของ Go 1 และคาดว่าโปรแกรม Go แทบทั้งหมดจะยังคอมไพล์และรันได้เหมือนเดิม
- ใน workspace สามารถใช้ ไดเรกทอรี vendor ที่สร้างด้วย
go work vendorได้ และถ้ามีvendorใน workspace ค่าเริ่มต้นของ-modจะเป็นvendor - ยุติการรองรับ
go getนอกโมดูลในโหมด legacyGOPATHเมื่อGO111MODULE=offส่วนคำสั่ง build อื่น ๆ เช่นgo buildและgo testยังคงทำงานกับโปรแกรม legacyGOPATHต่อไป - runtime ถูกเปลี่ยนให้วาง metadata ของ GC ตามชนิดไว้ใกล้กับ heap object ทำให้ประสิทธิภาพ CPU ของโปรแกรม Go ดีขึ้น 1–3% และลด overhead ของหน่วยความจำในหลายโปรแกรมลงประมาณ 1%
- การจัดแนวที่อยู่ของ object บางส่วนอาจเปลี่ยนจากเดิมที่ 16 ไบต์ขึ้นไปเป็นการจัดแนว 8 ไบต์ โค้ด assembly ที่สมมติว่ามีการจัดแนวสูงกว่านี้ต้องแก้ไข และสามารถใช้
GOEXPERIMENT=noallocheadersเป็นทางเลี่ยงชั่วคราวได้ - การ build แบบ PGO สามารถ devirtualize การเรียกได้มากขึ้น และเมื่อเปิดใช้ PGO ในชุดโปรแกรม Go ตัวแทน ประสิทธิภาพ runtime ดีขึ้น 2–14%
- เพิ่มแพ็กเกจ
v2แรกใน standard library คือmath/rand/v2โดยไม่สืบทอดRead, มี global generator ที่ใช้ seed แบบสุ่มเสมอ, มี sourceChaCha8และPCG, และมีฟังก์ชัน genericN - รูปแบบ routing ของ
net/http.ServeMuxรองรับ method และ wildcard แล้ว โดยมีการแตกความเข้ากันได้ย้อนหลังเล็กน้อยใน pattern ที่มี{}และการจัดการ escaped path และสามารถคืนพฤติกรรมเดิมได้ด้วยGODEBUG=httpmuxgo121=1 - ค่า default minimum version ของเซิร์ฟเวอร์
crypto/tlsเมื่อไม่ได้ระบุถูกเปลี่ยนเป็น TLS 1.2 และ cipher suite ที่ไม่มี ECDHE ใน handshake ก่อน TLS 1.3 จะไม่ถูก提供เป็นค่า default - execution tracer ถูกปรับปรุงใหม่ทั้งหมด โดยใช้ OS clock บนแพลตฟอร์มส่วนใหญ่ และให้ข้อมูล trace แบบแบ่งส่วนที่ประมวลผลแบบ streaming ได้, duration ของ syscall ทั้งหมด, และข้อมูล OS thread ที่ goroutine เคยรัน
- Web UI ของเครื่องมือ
traceถูกอัปเดตเป็นส่วนหนึ่งของงานรองรับ tracer ใหม่ และรองรับการแสดง thread-oriented view กับ duration ของ syscall ทั้งหมดใน trace ของโปรแกรมที่ build ด้วย Go 1.22 ขึ้นไป vetปรับคำเตือน loop closure ให้สอดคล้องกับความหมายของตัวแปรลูปใน Go 1.22 และเพิ่มคำเตือนสำหรับappendที่ไม่มีการใช้ค่า,time.Sinceที่ไม่ถูก defer ภายในdefer, และ key/value ของlog/slogที่ไม่ตรงกัน- การ bootstrap ต้องใช้ Go 1.20 รุ่น point release สุดท้ายขึ้นไป และคาดว่า Go 1.24 จะต้องใช้ Go 1.22 รุ่น point release สุดท้ายขึ้นไป
- บน
darwin/amd64Go toolchain จะสร้าง PIE เป็นค่า default และ Go 1.22 เป็นรุ่นสุดท้ายที่รันบน macOS 10.15 Catalina ส่วน Go 1.23 จะต้องใช้ macOS 11 Big Sur ขึ้นไป - เพิ่มชนิด
Aliasและฟังก์ชันUnaliasในgo/typesและเมื่อใช้gotypesalias=1client ต้องจัดการชนิดAlias slices.Delete,DeleteFunc,Compact,CompactFunc,Replaceจะ zero องค์ประกอบในช่วงที่ถูกย่อ และInsertจะ panic เสมอเมื่อiอยู่นอกช่วง- ฟังก์ชันอ่านไดเรกทอรีบน Windows อ่านรายการเป็น batch เพื่อลดจำนวน system call ทำให้ประสิทธิภาพดีขึ้นสูงสุด 30%
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตลอด 3 ปีที่ผ่านมาใช้ TypeScript เป็นหลัก แล้วเพิ่งเริ่มใช้ Go ไม่นานนี้ ตอนแรกก็กังวลนิดหน่อยเรื่องการขาดฟังก์ชันสำหรับอาร์เรย์และระบบ type ที่ยืดหยุ่นน้อยกว่า
แต่พอลองตั้งโปรเจ็กต์ TypeScript ใหม่สำหรับทีมขนาด 4~5 คน ก็พบว่าเสียเวลาไปมากกับการ linting, การเลือกไลบรารี server routing, การเลือกเซิร์ฟเวอร์, มาตรฐานการเขียนโค้ด, การจัดการข้อผิดพลาดพื้นฐาน, การบังคับใช้ custom error หรือชนิด
Resultเพื่อหลีกเลี่ยงนรกของtry/catchที่ซ้อนกัน, การทดสอบและ mocking, การตั้งค่า Prisma ฯลฯถึงจะชอบ TypeScript แต่การได้ใช้ภาษาที่มี standard library ที่ยอดเยี่ยม, type safety ที่เหมาะสม, และมาตรฐานการเขียนโค้ดที่มีมาให้ในตัว มันสะดวกมากจริงๆ Go ก็มี quirks อยู่บ้าง แต่ถือว่าดีมาก และการที่ routing เข้ามาอยู่ใน standard library ด้วยก็ยิ่งลดเรื่องที่ต้องคอยคิดลงไปอีกอย่าง ตอนนี้ก็รอแค่ฟังก์ชัน
map/filter/findสำหรับ sliceTypeScript ยิ่งออกรีลีสก็ยิ่งซับซ้อนขึ้นเรื่อยๆ เพื่อผู้ใช้ระดับสูง ขณะที่ Go ใส่สิ่งที่ขาดไปอย่าง
rangeสำหรับจำนวนเต็มเข้ามาเพื่อให้ใช้งานได้ง่ายขึ้นมันคล้ายกับภาคต่อของเกมที่ค่อยๆ เพิ่มการตั้งค่าและกลไกจนสุดท้ายต้องรีเซ็ตใหม่เพื่อผู้เล่นหน้าใหม่ แต่กับภาษาโปรแกรมมันทำแบบนั้นยากกว่า ดังนั้นก็ดีที่ Go ตระหนักถึงเรื่องนี้
go:embedกับgo:generateก็น่าประทับใจทุกครั้งที่เห็นในโปรเจ็กต์อื่นแพ็กเกจ
golang.org/xก็ดีมากเพราะมี implementation ที่เกี่ยวกับ RFC อินเทอร์เน็ตแทบทั้งหมดเตรียมไว้ให้ใช้ได้ทันทีmap/filter/findสำหรับ slice ก็ใช้ ไลบรารี lo ที่ได้แรงบันดาลใจจาก lodash ได้เป็นเครื่องมือสารพัดประโยชน์ที่ใช้บ่อยในโปรเจ็กต์ Go: https://github.com/samber/lo
คือพออ่านเอกสารเพื่อจะทำอะไรสักอย่าง ไม่นานก็จะมีใครสักคนมาบอก ทริกที่ใช้ได้จริง ซึ่งไม่ได้อยู่ในเอกสาร วนแบบนี้ซ้ำไปเรื่อยๆ
ถ้ารู้สึกว่า release notes ทางการค่อนข้างแห้งไปหน่อย มีเวอร์ชันแบบ interactive ทำไว้แล้ว: https://antonz.org/go-1-22
forทันที แต่พอลองรันเองแล้วเห็นผลลัพธ์ที่ไม่คาดคิดก็ช่วยได้มากCompactกับReplaceทำให้งงนิดหน่อย ไม่แน่ใจว่านี่ถูกต้องหรือเปล่าใช้ Go มามากกว่า 9 ปี และ 4 ปีหลังมานี้ใช้ Dart เยอะเพราะ Flutter ซึ่งมองว่าสองภาษานี้มี ท่าทีต่อความซับซ้อน ตรงกันข้ามสุดๆ Dart พยายามเพิ่มทุกฟีเจอร์ที่เป็นไปได้ ส่วน Go ตรงข้าม
อย่างแรก ใน Dart มีวิธี initialize ค่าในคลาสมากเกินไปจนเสียเวลาเยอะ ตำแหน่งที่ต้อง initialize ก็เปลี่ยนไปตามคำนำหน้า
final/const/static/lateไม่ว่าจะเป็น constructor, factory, หรือinitState()ของ FlutterStatefulWidgetและถ้ารีแฟกเตอร์ก็ต้องแก้โครงสร้างการ initialize ครั้งใหญ่ getter ที่ดูเหมือนตัวแปรแต่จริงๆ เป็นฟังก์ชันก็ยิ่งเพิ่มความสับสนใน codebase ใหม่อย่างที่สอง Dart มีฟีเจอร์อย่าง map, stream ฯลฯ ที่เหมาะกับการยัดทุกอย่างลงในบรรทัดเดียว เลยชวนให้เขียน โค้ดบรรทัดเดียว ที่เข้าใจยากได้อย่างรวดเร็ว แต่เวลา debug หรือเวลานักพัฒนารุ่นจูเนียร์เจอทั้งแฮ็กอย่าง
firstWhereOrNullและพารามิเตอร์ทางเลือกอย่างorElse: () => nullก็สับสนมาก แล้วยังมี null safety หรือ type error ที่เข้าใจยากจนทำงานต่อไม่ได้บ่อยๆ แต่พอเปลี่ยนมันเป็นลูปforแบบ Go ที่เรียบง่ายก็รู้สึกโล่งใจจริงๆinitStateเช่น ถ้าไม่ใช่ระดับตั้งค่า hint ของ text field ก็ควรหลีกเลี่ยงหน้า Flutter ส่วนใหญ่มีสิ่งที่ต้องทำไม่มาก หรือไม่ก็ให้ parent สร้างอ็อบเจ็กต์
MyPageControllerแล้ว initialize ตามต้องการ จากนั้น child page ก็ทำงานตาม controller นั้น ตัวอย่างเช่น ตอนผู้ใช้จะแก้ไขรายการในหน้ารายการ ก็สร้าง controller ใส่รายการลงไปแล้วส่งให้หน้าลูก พอกลับมาแล้ว parent ก็สามารถดูรายการใน controller หรือตัวแปรและ callback ต่างๆ เพื่อตัดสินใจว่าจะเปลี่ยน UI อย่างไรวิธีนี้ช่วยควบคุมการโต้ตอบระหว่างวิดเจ็ตได้ละเอียด โดยไม่ต้องไปแตะพวก
InheritedWidgetแน่นอนว่าควรใช้ร่วมกับไลบรารีจัดการสถานะ แต่ในทางปฏิบัติก็มีหลายกรณีที่ไม่ได้ใช้rangeก็เช่นกัน สุดท้ายจำนวนตัวอักษรที่ต้องพิมพ์ก็ใกล้เคียงกับ comprehension แบบforEach(f)เพียงแต่กระจายเป็นสามบรรทัดเลยดูยาวกว่า และถ้าเขียนออกมาตรงๆ ก็ไม่ได้มีอะไรใหญ่โตแต่ในเรื่องการ initialize ก็อยากให้ Go มีวิธีที่เป็นสำนวนมาตรฐานสำหรับ initialize ฟิลด์ของ struct ให้เป็นค่าบางอย่าง ถึงจะไม่มี constructor ก็ไม่เป็นไร แต่บางครั้งก็อยากให้
boolถูก initialize เป็นtrueโดยปริยายinitStateเป็นการ override ของStatefulWidgetและไม่ได้ต่างจาก lifecycle event ในเฟรมเวิร์กอื่นๆ ทั้งยังเป็นแนวคิดฝั่ง Flutter จึงไม่ใช่อะไรที่น่าจะสับสนกับ constructorอาจจะสับสนได้ว่าควรใช้ constructor ปกติหรือ factory แต่จะบอกว่า Go เหนือกว่าด้วยเหตุผลเรื่อง
initStateก็คงยากตอน
io.CopyคัดลอกจากTCPConnไปยังUnixConnถ้าเป็นไปได้จะใช้ system callsplice(2)ของ Linux และจะใช้เมธอดใหม่TCPConn.WriteToเท่ากับว่าเกิด zero-copy I/O เพิ่มขึ้นอีกครั้งแบบโปร่งใสจากการอัปเกรดอินเทอร์เฟซ ผลที่ได้จากแพตเทิร์นนี้ในแพ็กเกจ
ioมีขนาดใหญ่มากจริงๆถึงอย่างนั้นก็ยังถือว่าอยู่ในขอบเขตที่คาดหมายได้
แพตเทิร์นการทำ routing ที่ดีขึ้น เป็นการปรับปรุงที่ดีในช่วงที่อนาคตของ Gorilla Mux ดูไม่แน่นอน
แต่การเปลี่ยนแปลงนี้ก็ทำให้ความเข้ากันได้ย้อนหลังพังเล็กน้อย โดยมีทั้งส่วนที่ชัดเจน เช่น พฤติกรรมของแพตเทิร์นที่มี
{และ}เปลี่ยนไป และส่วนที่ชัดเจนน้อยกว่า เช่น การจัดการเส้นทางที่ถูก escape ไว้ โดยควบคุมได้ผ่านฟิลด์httpmuxgo121ของGODEBUGและถ้าตั้งhttpmuxgo121=1ก็จะย้อนกลับไปใช้พฤติกรรมเดิมได้แต่ก็ยังสงสัยว่าทำไมสิ่งนี้ถึงไม่ขัดกับ คำมั่นเรื่องความเข้ากันได้ของ Go 1 ที่บอกว่า “โปรแกรมที่เขียนตามข้อกำหนดของ Go 1 จะยังคงคอมไพล์และทำงานได้อย่างถูกต้องต่อไปตลอดอายุของข้อกำหนดนั้นโดยไม่ต้องเปลี่ยนแปลง”
godirective ในgo.modถ้าเป็น 1.22 ขึ้นไปก็จะได้พฤติกรรมใหม่ของไลบรารี ถ้าต่ำกว่านั้นก็ได้พฤติกรรมเดิม จะได้ผลดีแค่ไหนคงต้องดูกันต่อไป
{และ}ในเส้นทางของ HTTP handler แทบไม่มีและยังมีวิธีปิดพฤติกรรมใหม่ได้ จึงไม่ได้บังคับให้ต้องแก้โค้ด เพียงแต่ต้องตั้งตัวแปรแวดล้อมตัวใหม่
การเปลี่ยนความหมายของลูป
forในรีลีสนี้ก็เป็นตัวอย่างคล้ายกัน คือในทางปฏิบัติมันเป็นการเปลี่ยนที่ทำของเดิมพัง แต่โปรแกรม Go ทั้งหมดยังคอมไพล์และรันได้ต่อ เพียงแค่บางพฤติกรรมเปลี่ยนไปนิดหน่อย มองว่า Go เลือกแนวทางที่เน้นการใช้งานจริง และนั่นก็เป็นหนึ่งในเหตุผลที่มันประสบความสำเร็จgo.modเพื่อทำให้เวอร์ชันต่างกันพังต่างกัน อย่างน้อยก็ควรทำให้พังในแบบที่ดีกว่านี้เวลาปรับ
go.modเป็น 1.22 ถ้าให้ คอมไพล์ไม่ผ่าน ไปเลยยังดีกว่าต้องไปไล่หาปัญหา runtime ที่ละเอียดอ่อนใน Go 1.22 แต่ละรอบของลูปจะสร้างตัวแปรใหม่
เรื่องนี้เคยคุยกันที่นี่มาก่อนแล้ว: https://news.ycombinator.com/item?id=33160236 - Go: Redefining For Loop Variable Semantics (2022)
อาจจะหัวโบราณไปหน่อย แต่ไม่ชอบการเพิ่ม
rangeสำหรับฟังก์ชัน มองว่าความสะดวกที่ได้ยังไม่มากพอจะคุ้มกับความซับซ้อนที่เพิ่มเข้ามาในภาษา และสไตล์แบบ functional ก็ดูไม่เข้ากับสไตล์ของ Go ที่ชัดเจน เป็นเชิง imperative และค่อนข้าง verbose แต่มีฟีเจอร์ไม่มาก ซึ่งนั่นเคยเป็นจุดแข็งสำคัญอย่างหนึ่งของมันด้วยเหตุผลเดียวกัน จึงคิดว่า
rangeสำหรับจำนวนเต็มก็เป็นทิศทางที่ผิดเช่นกัน ชุดฟีเจอร์ที่บางและภาระทางความคิดที่ต่ำของ Go เป็นทั้งจุดแข็งหลักและจุดที่ทำให้มันแตกต่างมาโดยตลอดปัญหาใหญ่ข้อหนึ่งของการพัฒนาซอฟต์แวร์คือการห้ามไม่ให้นักพัฒนาเพิ่มความซับซ้อนโดยไม่จำเป็นหรือโชว์ฝีมือเกินเหตุ โดยเฉพาะคนที่ยังอยู่ช่วงต้นอาชีพ มักมีแนวโน้มจะทำวิธีแก้ให้ซับซ้อนเกินไป แทนที่จะเขียนแบบตรงไปตรงมาซึ่งอาจดูไม่หรูและยาวกว่า
ตอนนี้การรองรับ generics ของ Go ยังไม่ละเอียดพอจะรองรับสิ่งนี้ได้ดี จึงเกิดข้อจำกัดแปลก ๆ ที่ดูเหมือนถูกยัดเข้ามา
การเพิ่ม
sql.Null[T]ถือว่าดี น่าจะเริ่มใช้ในโปรเจ็กต์ใหม่ในงานปัจจุบันยังพึ่งพา null [0] ของ sqlboiler อยู่ ซึ่ง API คล้ายกันมาก ทำงานแบบ
sql.Nullแต่มีเมธอดIsSet() boolเพิ่มมาเพื่อบอกว่าค่านั้นเคยถูกตั้งไว้อย่างชัดเจนหรือไม่ ช่วยแยก “null โดยตั้งใจ” ออกจาก “null เพราะยังไม่ถูกกำหนดค่า” ฟังดูดี แต่ในทางปฏิบัติก็ไม่เคยได้ใช้เลยตั้งใจว่าจะกลับไปดู mux ใน standard library อีกรอบ และหวังว่าอาจจะ ถอด chi ออก ได้ด้วย ชอบมากที่ฟีเจอร์แบบนี้เข้ามาอยู่ใน standard library
พอฟีเจอร์แบบนี้เข้า standard library ก็มั่นใจได้ว่าจะมีการดูแลรักษาตลอด และโปรแกรม Go จำนวนมากจะใช้แนวทางเดียวกัน
ถ้าใช้ Go ใน production สงสัยว่าปกติย้ายไปเวอร์ชันใหม่กันได้เร็วแค่ไหน หรือมักจะติดอยู่กับรีลีสเก่า
เวลาดูโปรเจ็กต์ที่เปิดเผยต่อสาธารณะ รู้สึกว่าหลายแห่งค่อนข้างเลี่ยงการใช้ฟีเจอร์ใหม่ ๆ ถึงจะยังไม่ใช้ generics แต่อยากใช้
anyที่มาจาก Go 1.18 แทนinterface{}แบบเดิม เพราะก็เคยได้ยินว่าตัวหลังดู “เป็นสำนวนของ Go” มากกว่ารอบนี้จังหวะมันแปลกนิดหน่อย เพราะทีม Go ออก 1.22 มาก่อนที่เราจะยกค่าเริ่มต้นเป็น 1.21 ตอนนี้การรอกลายเป็นแทบจะเป็นธรรมเนียมไปแล้ว และก็ไม่ค่อยเกิดกรณีที่รีลีสของ Go ทำของเดิมพัง
อย่างเช่น generics เรารออยู่ราว 6 เดือนกว่าจะเริ่มใช้จริง เพื่อดูทั้งประโยชน์ ประสบการณ์ของนักพัฒนา และผลกระทบต่อความเร็วในการ build/test ว่าไม่ได้มากเกินไป
แต่เราก็ส่งมอบแอปสำหรับผู้ใช้ปลายทางที่เขียนด้วย Go ด้วย ซึ่งฝั่งนั้นต้องค้างอยู่ที่ 1.20 เพราะต้องรองรับ client OS รุ่นเก่า และใน 1.21 มีการตัดการรองรับออกไปพอสมควร จึงมีโอกาสสูงว่าจะอยู่ที่ 1.20 ต่อไปอีกหลายปี
latestของ Go และชุดทดสอบก็ค่อนข้างแข็งแรง เลยแทบจะเป็นการอัปเดตอัตโนมัติอยู่แล้ว ถ้ามีอะไรไม่เข้ากัน pipeline ก็จะพัง แล้วค่อยตัดสินใจว่าจะตรึงไว้ที่เวอร์ชันล่าสุดที่ยังใช้ได้ หรือจะเลื่อนการอัปเดตออกไปจนกว่าจะแก้การเปลี่ยนแปลงที่ทำให้พังได้ประมาณไม่กี่ปีครั้งก็จะมีบั๊ก edge case ละเอียดอ่อนระหว่างเวอร์ชันใหม่กับแพ็กเกจ third-party โผล่มา แต่บั๊กแบบนั้นตามนิยามก็คือหลุดรอดชุดทดสอบอยู่แล้ว ดังนั้นถึงจะอัปเกรดแบบ manual ก็น่าจะจับไม่เจอเหมือนกัน
ถ้ากังวลเรื่องการรองรับคอมไพเลอร์ Go รุ่นเก่า ก็ใส่ build condition สำหรับเวอร์ชันเก่าไว้เพื่อกำหนดสิ่งที่ขาดได้ เช่น ใต้
// +build !go1.7ก็สามารถใส่type any = interface{}ได้