- Go 1.21 มุ่งลดภาระในการอัปเกรด โดยขยาย ความเข้ากันได้ผ่าน GODEBUG เพื่อให้ toolchain ใหม่จำลองพฤติกรรมของ Go เวอร์ชันเก่าได้อย่างเสถียรที่สุด
- ตั้งแต่ Go 1 ในปี 2012 Go ได้ให้คำมั่นเรื่อง ความเข้ากันได้ของซอร์สโค้ด และลดความเสียหายจากการลบหรือเปลี่ยนแปลงผ่านการตรวจสอบ API สาธารณะและการทดสอบภายในขนาดใหญ่
- แม้แต่การปรับปรุงที่เอกสารอนุญาต เช่น การเพิ่มความละเอียดของ
time.Now, การเปลี่ยน implementation ของ sort, การเปลี่ยน output ของ compress/flate, การขยาย input ของ strconv.ParseInt, และการเปลี่ยน parsing ของ net.ParseIP ก็อาจทำให้โปรแกรมเดิมพังได้
- ตั้งแต่ Go 1.21 การตั้งค่า GODEBUG เพื่อความเข้ากันได้จะคงอยู่ไม่น้อยกว่า 2 ปี หรือ 4 release ของ Go และสามารถรักษาพฤติกรรมเดิมเป็นค่า default ตามเวอร์ชัน
go ใน go.mod ได้
- Go 2 จะไม่ออกมาในรูปแบบ specification ใหม่ที่ทำให้โปรแกรม Go 1 พัง และแม้ Go จะเพิ่มฟีเจอร์ใหม่ ก็ยังให้ ความเข้ากันได้มาก่อน เพื่อให้การอัปเกรด toolchain มีเสถียรภาพ
หลักการพื้นฐานของความเข้ากันได้ใน Go 1
- ในปี 2012 Go ได้เผยแพร่เอกสาร “Go 1 and the Future of Go Programs” พร้อมกับ Go 1 โดยตั้งเป้าว่าโปรแกรมที่เขียนตาม specification ของ Go 1 จะยังคง compile และทำงานได้อย่างถูกต้องโดยไม่ต้องเปลี่ยนแปลงตลอดอายุของ specification นั้น
- แกนหลักของคำมั่นนี้คือ ความเข้ากันได้ของซอร์สโค้ด
- เมื่ออัปเดตเป็น Go เวอร์ชันใหม่ โค้ดต้องถูก compile ใหม่
- สามารถเพิ่ม API ใหม่ได้ แต่ควรหลีกเลี่ยงการเพิ่มในลักษณะที่ทำให้โค้ดเดิมพัง
- ไม่มีการเปลี่ยนแปลงในอนาคตใดที่รับประกันได้ว่าจะไม่ทำให้ทุกโปรแกรมพังอย่างเด็ดขาด
- หากโปรแกรมพึ่งพาพฤติกรรมที่เป็น bug เมื่อ bug นั้นถูกแก้ โปรแกรมก็อาจพังได้
- Go พยายามรักษาการอัปเกรดให้เสถียร พร้อมลดความเสียหายให้ได้มากที่สุด
การป้องกันการทำลายความเข้ากันได้ด้วยการตรวจสอบ API สาธารณะ
- ในกระบวนการพัฒนา Go จะดูแลรายการ API สาธารณะ ของแต่ละ package ไว้ในไฟล์แยกจาก package จริง
- ตัวอย่างเช่น
go/api/go1.21.txt บันทึกรายการ function, method และ type ของ bytes, cmp, context เป็นต้น
- การทดสอบมาตรฐานจะตรวจสอบว่า API จริงของ package ตรงกับไฟล์เหล่านี้หรือไม่
- หากเพิ่ม API ใหม่ ต้องเพิ่มลงในไฟล์ API ด้วย การทดสอบจึงจะผ่าน
- หากเปลี่ยนหรือลบ API เดิม การทดสอบก็จะล้มเหลว
- ไม่ใช่แค่การลบ API เท่านั้น การเปลี่ยน type ก็ทำลายความเข้ากันได้ได้เช่นกัน
os.Stdout เป็น global variable ของ type *os.File
- หากเปลี่ยนเป็น interface ที่มี method เดียวกัน โค้ดที่ต้องการ
*os.File เช่น greet(f *os.File) จะพัง
- การตรวจสอบ API มีประโยชน์ในการจับการเปลี่ยนแปลงหรือลบ API แต่ไม่สามารถป้องกัน การเปลี่ยนแปลงที่ไม่เข้ากัน ทุกแบบที่อาจเกิดขึ้นใน Go ได้
ความเสียหายละเอียดอ่อนที่การทดสอบเผยให้เห็น
- เวอร์ชันพัฒนาของ Go release ใหม่จะถูกทดสอบอย่างต่อเนื่องกับโค้ด Go ภายในของ Google ทั้งหมด
- หากการทดสอบผ่าน commit นั้นจะถูกติดตั้งเป็น production Go toolchain ของ Google
- หากการทดสอบภายในพัง จะถือว่าโค้ดภายนอกก็อาจพังได้ และจะหาวิธีลดผลกระทบ
- ในกรณีส่วนใหญ่ จะย้อนการเปลี่ยนแปลงกลับ หรือเขียนใหม่เพื่อไม่ให้โปรแกรมพัง
- การเปลี่ยนแปลงบางอย่าง แม้ทำให้โปรแกรมพัง ก็อาจสำคัญและยังถือเป็น การเปลี่ยนแปลงที่เข้ากันได้ตามเอกสาร
- แม้ในกรณีนี้ ก็จะลดขอบเขตผลกระทบและระบุปัญหาที่อาจเกิดขึ้นไว้ใน release notes
สองกรณีที่พบใน Go 1.1
-
struct literal และ field ใหม่
- ใน Go 1
net.TCPAddr เป็น struct ที่มีสอง field คือ IP และ Port และ composite literal แบบไม่ระบุชื่อ field ก็ compile ได้
- เมื่อ Go 1.1 เพิ่ม field
Zone เข้าไปใน net.TCPAddr โค้ดเดิมจึง compile ไม่ผ่านด้วย error “too few initializers in struct literal”
- วิธีเขียนที่เข้ากันได้คือใช้ literal แบบมี tag
var myAddr = &net.TCPAddr{
IP: net.IPv4(18, 26, 4, 9),
Port: 80,
}
- หากไม่ระบุ
Zone field นั้นจะใช้ zero value คือสตริงว่าง
- เอกสารความเข้ากันได้ระบุให้ใช้ composite literal แบบมี tag สำหรับ struct ใน standard library และ
go vet จะรายงาน literal แบบไม่มี tag ที่จำเป็นต่อความเข้ากันได้กับเวอร์ชันหลัง ๆ
-
ความละเอียดของเวลา
- หลัง Go 1
time.Now ถูกเปลี่ยนให้คืนค่าความละเอียดระดับ nanosecond แทน microsecond
- การเปลี่ยนแปลงนี้อาจทำให้ test ที่คาดหวังว่าค่าของ
time.Now จะเหมือนเดิมหลัง round-trip ผ่าน save และ load พังได้
- หากรูปแบบการบันทึกเก็บได้เพียงความละเอียดระดับ microsecond ใน Go 1 จะสำเร็จ แต่ใน Go 1.1 อาจล้มเหลว
- Go เพิ่ม method
Round และ Truncate เพื่อช่วยแก้ test แบบนี้ และสรุปปัญหาที่อาจเกิดขึ้นพร้อม method ใหม่ไว้ใน release notes
- ความละเอียดที่ดีขึ้นเป็นพฤติกรรมที่ดีกว่าและอยู่ในขอบเขตที่ function document ไว้ จึงถูก release แม้จะทำให้บางโปรแกรมพัง
การเปลี่ยนแปลงสามประเภทที่อาจทำลายความเข้ากันได้
-
การเปลี่ยน output
- การเปลี่ยน output เกิดขึ้นเมื่อ function ให้ output ต่างจากเดิม แต่ output ใหม่นั้นถูกต้องเท่าเดิมหรือถูกต้องยิ่งกว่า
- ตัวอย่างสำคัญคือการเพิ่มความละเอียดระดับ nanosecond ของ
time.Now
- ใน Go 1.6 implementation ของ
sort ถูกเปลี่ยนให้เร็วขึ้นราว 10% ทำให้ลำดับของ element ที่ถือว่ามีค่าเท่ากันเปลี่ยนไป
- output ของ Go 1.5:
[red blue green white black yellow orange indigo violet]
- output ของ Go 1.6:
[red blue white green black orange yellow indigo violet]
- การ sort อนุญาตให้คืนผลลัพธ์ที่เท่ากันในลำดับใดก็ได้ แต่โปรแกรมที่คาดหวังลำดับเฉพาะจะพัง
- ใน Go 1.8
compress/flate ถูกปรับปรุงให้สร้าง output ที่เล็กลงโดยใช้ CPU และ memory overhead ใกล้เคียงเดิม
- ด้วยเหตุนี้ build archive แบบ reproducible ภายใน Google จึงไม่สามารถสร้าง archive เดิมซ้ำได้เหมือนเดิม
- โครงการนั้น fork
compress/flate และ compress/gzip เพื่อคง algorithm เดิมไว้
- เพื่อเตรียมรับการเปลี่ยน output ควรเขียนโปรแกรมและ test ให้ยอมรับ output ที่ถูกต้องทุกรูปแบบ
- หากต้องการ output ที่ reproducible อย่างแท้จริง ก็สามารถ fork โค้ดได้ แต่จะหลุดออกจาก bug fix ด้วยเช่นกัน
-
การเปลี่ยน input
- การเปลี่ยน input เกิดขึ้นเมื่อ function เปลี่ยน input ที่ยอมรับหรือวิธีประมวลผล
- Go 1.13 เพิ่ม syntax underscore เพื่อให้อ่านตัวเลขง่ายขึ้น และ
strconv.ParseInt ก็ถูกเปลี่ยนให้ยอมรับ syntax ใหม่นี้
- โค้ดของผู้ใช้ภายนอกที่ใช้ตัวเลขคั่นด้วย underscore เป็นรูปแบบข้อมูลแยกต่างหากจึงพัง
- โค้ดนั้นลอง
ParseInt ก่อน และจะจัดการ underscore เฉพาะเมื่อ parse ล้มเหลว แต่ ParseInt ไม่ล้มเหลวอีกต่อไป
net.ParseIP เคยอนุญาต IP address แบบเลขฐานสิบที่มีศูนย์นำหน้า ตามตัวอย่างใน RFC รุ่นแรก ๆ ของ IP
- Go อ่าน
18.032.4.011 เป็น 18.32.4.11
- C library ตระกูล BSD ตีความศูนย์นำหน้าเป็นจุดเริ่มของเลขฐานแปด จึงอ่านสตริงเดียวกันเป็น
18.26.4.9
- Go 1.17 เปลี่ยนให้
net.ParseIP ปฏิเสธศูนย์นำหน้าไปเลย
- นี่เป็นการเลือกเพื่อให้เมื่อทั้ง Go และ C parse IP address สำเร็จ ความหมายจะตรงกัน
- Kubernetes กังวลว่า configuration ที่บันทึกไว้เดิมอาจ parse ไม่ได้ใน Go 1.17 และเริ่มใช้ fork ของ
net.ParseIP เดิม
- สำหรับ user input ควรตรวจสอบ syntax ที่จะยอมรับก่อน parse ค่า แต่ในบางกรณีอาจจำเป็นต้อง fork โค้ด
-
การเปลี่ยน protocol
- การเปลี่ยน protocol เกิดขึ้นเมื่อการเปลี่ยนแปลงใน package ปรากฏให้เห็นใน protocol ที่ใช้สื่อสารกับโลกภายนอก
- Go 1.6 เพิ่มการรองรับ HTTP/2 โดยอัตโนมัติ
- client ของ Go 1.5 ใช้เฉพาะ HTTP/1.1 จึงอาจทำงานปกติในสภาพแวดล้อมที่มีอุปกรณ์กลางเครือข่ายบางแบบ
- เมื่ออัปเดตเป็น Go 1.6 จะมีการใช้ HTTP/2 และหาก HTTP/2 ทำงานไม่ได้ในสภาพแวดล้อมนั้น โปรแกรมก็อาจพัง
- Go พยายามรองรับ protocol สมัยใหม่เป็นค่า default แต่การเปิดใช้ HTTP/2 ก็อาจทำให้โปรแกรมพังได้ แม้ไม่ได้เกิดจากความผิดของโปรแกรมหรือ Go เอง
- Go 1.6 สรุปการเปลี่ยนแปลงไว้ใน release notes และให้วิธีปิด HTTP/2
- ตั้งค่า field
TLSNextProto อย่างชัดเจน
- ตั้งค่า
GODEBUG=http2client=0, GODEBUG=http2server=0 หรือทั้งสองอย่างร่วมกัน
- การรองรับ HTTPS certificate ที่ใช้ SHA1 ก็เป็นกรณีการเปลี่ยน protocol ที่ละเอียดอ่อนกว่า
- Certificate authority หยุดออก certificate SHA1 ในปี 2015 และ browser หลัก ๆ เลิกยอมรับในปี 2017
- Go 1.18 ปิดการรองรับ certificate SHA1 เป็นค่า default และเปิดให้เลี่ยงได้ด้วย GODEBUG
- การติดตั้ง Kubernetes บางส่วนยังใช้ private certificate แบบ SHA1 อยู่ Go จึงตัดสินใจคงการตั้งค่า workaround ไว้นานกว่าที่วางแผนไว้
การรองรับ GODEBUG ที่ขยายใน Go 1.21
- Go 1.21 ขยายและทำให้การใช้ GODEBUG เป็นทางการ เพื่อลดปัญหาความเข้ากันได้ที่ละเอียดอ่อนยิ่งขึ้น
- สำหรับการเปลี่ยนแปลงที่กฎความเข้ากันได้ของ Go 1 อนุญาต แต่สามารถทำให้โปรแกรมเดิมพัง จะกำหนดการตั้งค่า GODEBUG เพื่อให้แต่ละโปรแกรมปฏิเสธพฤติกรรมใหม่ได้
- อาจมีกรณีที่เพิ่มการตั้งค่าไม่ได้ แต่ถือว่าเป็นกรณีที่พบได้น้อยมาก
- การตั้งค่า GODEBUG เพื่อความเข้ากันได้จะคงอยู่อย่างน้อย 2 ปี หรือ 4 release ของ Go
- การตั้งค่าอย่าง
http2client, http2server อาจคงอยู่นานกว่านั้นมาก หรือในบางกรณีอาจไม่มีกำหนดเลิก
- หากเป็นไปได้ การตั้งค่า GODEBUG แต่ละรายการจะเชื่อมกับ counter ใน
runtime/metrics
- ชื่อ counter อยู่ในรูปแบบ
/godebug/non-default-behavior/<name>:events
- ตัวอย่างเช่น เมื่อตั้งค่า
GODEBUG=http2client=0 ค่า /godebug/non-default-behavior/http2client:events จะนับจำนวน HTTP transport ที่ถูก configure โดยไม่มี HTTP/2
- ค่า default ของ GODEBUG สำหรับโปรแกรมจะถูกปรับให้ตรงกับเวอร์ชัน Go ที่ระบุใน
go.mod ของ main package
- หาก
go.mod เป็น go 1.20 และอัปเดตมาใช้ toolchain Go 1.21 พฤติกรรมที่ควบคุมด้วย GODEBUG ซึ่งเปลี่ยนใน Go 1.21 จะยังคงพฤติกรรมแบบ Go 1.20 จนกว่าจะเปลี่ยน go.mod เป็น go 1.21
- สามารถเปลี่ยนการตั้งค่า GODEBUG รายตัวได้ด้วยบรรทัด
//go:debug ใน package main
- การตั้งค่า GODEBUG ทั้งหมดสรุปอยู่ใน รายการกลาง
กรณี panic(nil)
- ใน Go 1.21
panic(nil) ตอนนี้ทำให้เกิด runtime panic ที่ไม่ใช่ nil
- การเปลี่ยนแปลงนี้ทำให้ผลลัพธ์ของ
recover บอกได้อย่างน่าเชื่อถือว่า goroutine ปัจจุบันกำลัง panic อยู่หรือไม่
- พฤติกรรมใหม่ถูกควบคุมด้วยการตั้งค่า GODEBUG และขึ้นอยู่กับบรรทัด
go ใน go.mod ของ main package
- หากเป็น
go 1.20 หรือต่ำกว่า panic(nil) จะยังได้รับอนุญาตต่อไป
- หากเป็น
go 1.21 ขึ้นไป panic(nil) จะเปลี่ยนเป็น panic ที่มี runtime.PanicNilError
- ค่า default ตามเวอร์ชันสามารถ override อย่างชัดเจนได้โดยเพิ่มบรรทัดต่อไปนี้ใน
package main
//go:debug panicnil=1
- ชุดกลไกนี้ช่วยให้ยังคงพฤติกรรมของ toolchain เดิมไว้ได้แม้อัปเดตเป็น toolchain ใหม่ ควบคุมเฉพาะการตั้งค่าที่จำเป็นได้ละเอียด และมองเห็นการใช้พฤติกรรมที่ไม่ใช่ค่า default ผ่าน production monitoring
- รายละเอียดเพิ่มเติมสรุปไว้ใน “Go, Backwards Compatibility, and GODEBUG”
Go 2 จะไม่ทำให้ Go 1 พัง
- เอกสาร “Go 1 and the Future of Go Programs” มีเงื่อนไขว่าในสักวันหนึ่งอาจมี specification ของ Go 2 ออกมา
- จะไม่มี Go 2 ในความหมายที่ทำให้โปรแกรม Go 1 ไม่สามารถ compile ได้อีกต่อไป
- Go 2 ในความหมายของการปรับปรุงครั้งใหญ่ของ Go 1 ที่เริ่มต้นในปี 2017 นั้นเกิดขึ้นไปแล้ว
- Go เห็นว่าความเข้ากันได้มีคุณค่ามากกว่าการตัดขาดจากอดีตอย่างมาก จึงเลือกทิศทางที่เสริมความเข้ากันได้ให้แข็งแรงขึ้น
- งานใหม่ ๆ ที่น่าสนใจจะยังคงดำเนินต่อไป แต่จะทำด้วยวิธีที่รอบคอบและเข้ากันได้ เพื่อให้การอัปเกรดระหว่าง toolchain เสถียรที่สุดเท่าที่จะเป็นไปได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
คำถามสำคัญเรื่องความเข้ากันได้ไม่ใช่ “จะทำหรือไม่” แต่คือ “จะทำอย่างไร” อันที่จริงมันใกล้เคียงกับการอยากให้โค้ดของฉันตั้งแต่ตอนนี้ทำงานต่อไปได้เรื่อย ๆ มากกว่าความเข้ากันได้ย้อนหลัง
Go 1.21 มอบหัวใจสำคัญสองอย่างที่ยากจะเห็นพร้อมกันในระบบนิเวศภาษาอื่น: ทุกการเปลี่ยนแปลงมี การตั้งค่า GODEBUG และสามารถย้อนกลับเป็นรายรายการได้ อีกทั้งยังมีตัวชี้วัดเพื่อตรวจจับว่ามีการใช้ implementation เดิมหรือไม่ นอกจากนี้ยังมี เวอร์ชัน toolchain ต่อโมดูล และสามารถดึง toolchain ของ Go ที่เก่ากว่าหรือใหม่กว่าเข้ามาโดยอัตโนมัติได้อย่างปลอดภัยเหมือนโมดูล
แถมถ้าระบุเวอร์ชันเฉพาะอย่าง
go 1.21.2แม้จะรันบน Go ที่ใหม่กว่า ก็จะปรับใช้การตั้งค่า opt-out ที่เกี่ยวข้องให้อัตโนมัติจนกว่าจะร้องขอพฤติกรรมใหม่อย่างชัดเจน สามารถประกาศได้ทุกที่ ไม่ว่าจะในโค้ด,go.modหรือ environment variable จึงเป็นวิธีที่เรียบง่ายและงดงามซึ่งครอบคลุมกรณีการใช้งานด้านความเข้ากันได้เกือบทั้งหมด ตั้งแต่นักพัฒนาไปจนถึงผู้ดูแลการ deployuse v5.24ไว้ที่ต้นไฟล์ ก็ทำให้ ทำงานเหมือน Perl 5.24 ได้ และมีผลในระดับไฟล์ ไม่ใช่ระดับโมดูลแค่เรื่องว่าจะรองรับเวอร์ชันใหม่หรือไม่ก็ทำให้เกิดปัญหาซับซ้อนอย่างเหลือเชื่ออยู่แล้ว และยังจะเกิดหุบเหวลึกแบบ “เราคิดว่าเรารองรับเวอร์ชันใหม่ และเวอร์ชันใหม่ก็คิดว่ารองรับเรา แต่ต่างฝ่ายต่างพลาดบางอย่าง” ขึ้นมาอีก
ผมชอบทิศทางแบบนี้มาก ไม่มีอะไรดีไปกว่าการเข้าไปใน codebase ของ Go แล้วคาดหวังได้ว่า แค่อัปเวอร์ชัน Go ทุกอย่างก็จะยังทำงานได้ดี
แต่ที่กังวลคือ type system นั้นยากจะปรับปรุงครั้งใหญ่ได้โดยไม่มี breaking change แบบ “นี่คือโค้ดที่ผิด ตอนนี้จึงคอมไพล์ไม่ผ่านแล้ว” ไม่รู้ว่าทีม Go สนใจเรื่องนี้หรือไม่ แต่มีผลไม้ที่อยู่ต่ำจำนวนมากที่เพิ่มความแข็งแรงตอน compile time ได้มากโดยไม่ต้องเพิ่มฟีเจอร์ภาษา
ตัวอย่างเช่น การรายงาน
nilที่ไม่ได้ตรวจสอบ, การตรวจสอบการเข้าถึง array, type inference สำหรับ nested struct literal, และ การตรวจสอบ enum แบบครบทุกกรณี โดยเฉพาะเวลาจะเขียน nested gRPC call แค่มี type inference เพิ่มอีกขั้นเดียวก็พอแล้ว เพราะ type นั้นมีอยู่แล้วใน signature ของฟังก์ชันที่เรียก แต่ใน Go มันเจ็บปวดเกินไปถ้า type system ของ Go จะดีขึ้น ก็อยากให้เพิ่ม algebraic data type ที่เรียบง่ายและงดงามแบบนี้ มันน่าจะเข้ากับ Go ได้ดีจริง ๆ จึงอยากให้มอบของขวัญนี้ให้ตัวเองดู
เห็นด้วยเต็มที่ เหตุผลที่ทำให้รอคอย release ของ Go คือมันมักเพิ่มสิ่งที่มีประโยชน์เข้ามาโดยไม่ทำให้อะไรพัง
ไม่มีส่วนไหนของภาษาที่ดูพังจนแก้ด้วยการเปลี่ยนแปลงที่เข้ากันได้ย้อนหลังไม่ได้ แม้กับกับดักไม่กี่อย่างอย่างการกำหนดค่า loop variable ก็โดยมากมีข้อเสนอที่ยังรักษาความเข้ากันได้
โดยส่วนตัวผมเข้าใจมุมมองที่คาดหวังต่อ release ของ Go แต่ความแตกต่างของท่าทีระหว่างสองระบบนิเวศนี้น่าสนใจ
ผมเคยสรุปบางส่วนของปัญหาที่เจอไว้ก่อนหน้านี้: https://news.ycombinator.com/item?id=29763324
Rust หรือ gcc นั้นเมื่ออัป compiler ก็ได้ฟีเจอร์ภาษาเพิ่ม แต่ไลบรารีซับซ้อนส่วนใหญ่ยังเหมือนเดิมและสามารถอัปแยกได้ ตัวอย่างเช่น HTTP ของ Rust แยกอยู่ที่
hyperส่วน C ใช้libcurlเป็นต้นตรงกันข้าม Go เมื่ออัป compiler สิ่งที่ตามมาด้วยไม่ใช่แค่ generics,
embed, และฟีเจอร์ toolchain แต่ยังรวมถึงการเปลี่ยนแปลงของtls, ไลบรารีความปลอดภัย, HTTP client/server ด้วย ก้อน standard library ใหญ่ ๆ อย่างhttp,tls,cryptoควรถูกแยกเป็นไลบรารีต่างหากจะดีกว่ามาก เพราะแบบนั้นเราจะอัป compiler ได้โดยไม่ต้องกลัว และอัปไลบรารีตามจังหวะของเราเองได้asyncเข้ามา ซึ่งใน 2015 edition สามารถสร้างตัวแปรหรือฟังก์ชันชื่อasyncได้ จึงเป็น breaking changeความเข้ากันได้ย้อนหลังเป็นเรื่องดี แต่ก็ยากจะมั่นใจว่าอนาคตที่ Go ทำ innovation X บางอย่างไม่ได้เพราะต้องรักษาความเข้ากันได้นั้นโอเคหรือไม่
ผมสนับสนุนอย่างมากกับจุดยืนที่ว่า Go 2 ในอนาคตจะไม่มีวันทำลายความเข้ากันได้กับ Go 1 ถ้าต้องเปลี่ยนภาษาใหญ่ขนาดนั้น ผมว่าก็ควร fork แล้วเปลี่ยนชื่อไปเลยจะดีกว่า
แต่ก็สงสัยว่าทำไมไม่ไปให้สุดกว่านี้แล้วพูดว่า “จะไม่มี Go 2” เพื่อกำจัดความคลุมเครือไปเลย ถ้า Go 2 ในเชิงทฤษฎีสามารถรันโปรแกรม Go 1 ได้ทั้งหมด แล้วมันต่างจากรีลีส Go 1.xx ตรงไหน บทความบอกว่า “จะไม่มี Go 2 ที่ทำให้โปรแกรม Go 1 พัง” แต่ดูเหมือนจะไม่ได้พูดไปไกลกว่านั้น
https://github.com/golang/proposal/blob/d661ed19a203000b7c54...
อธิบายว่าหากกระบวนการข้างต้นทำงานได้ตามแผน ในความหมายสำคัญแล้ว Go 2 ก็จะไม่มีอยู่จริง และจะเป็นการค่อย ๆ เปลี่ยนผ่านด้วยฟีเจอร์ภาษาและฟีเจอร์ไลบรารีใหม่ ๆ อาจมีสักจุดที่ในเชิงการตลาดเรียกว่า “ตอนนี้คือ Go 2” ก็ได้ หรืออาจข้ามไปเลยก็ได้ ตรรกะคือ C 2.0 ยังไม่มี แล้วทำไม Go 2.0 ถึงจำเป็น
ภาษา लोकप्रियอย่าง C, C++, Java ก็แทบจะเป็นเวอร์ชัน 1.N มาโดยตลอด และผมคิดว่า Go ก็ควรทำตามนั้น Go 2 ตัวจริงในความหมายของภาษาใหม่หรือไลบรารีหลักใหม่ที่ไม่เข้ากันกับของเดิม ไม่ใช่ทางเลือกที่ดีสำหรับผู้ใช้ และอาจเป็นอันตรายได้
สุดท้ายผมคิดว่าวิธีนั้นก็สมเหตุสมผล
ถ้ามีฟีเจอร์ภาษาใหม่ที่ชุมชนต้องการอย่างชัดเจน และวิธีที่ถูกต้องหรือวิธีเดียวที่จะใส่ฟีเจอร์นั้นได้คือการเพิ่ม keyword กฎที่ว่าห้ามทำลาย source compatibility เด็ดขาดก็จะทำให้ฟีเจอร์นั้นไม่มีวันถูกเพิ่มได้เลย ผมเห็นด้วยกับการเปลี่ยนแปลงใหญ่ ๆ ที่เปลี่ยน semantics ของภาษา แต่การเปลี่ยนแปลงแบบไม่เข้ากันย้อนหลังไม่ได้เป็นการเปลี่ยนแปลงมหึมาเสมอไป
ในเชิงทฤษฎี แนวคิดที่ว่า backward compatibility จะไม่มีวันเปลี่ยนนั้นดี แต่ในความเป็นจริงก็มี breaking change ที่มีความหมายอยู่เหมือนกัน การต้องแบกฟีเจอร์ภาษาที่ตอนออกแบบครั้งแรกไม่ได้คำนึงถึง X หรือ Y ไปตลอดกาล ไม่ได้รู้สึกว่าเป็นประโยชน์เสมอไป
ใช้ Go เยอะมาก และทิศทางแบบนี้ทำให้รู้สึกอบอุ่นใจจริง ๆ
ความเข้ากันได้อาจไม่ใช่เรื่องสนุกสำหรับทีมภาษาเท่าไร เพราะต้องยืนขาข้างหนึ่งไว้กับ “อดีตอันไกลโพ้น” อย่างมั่นคงอยู่เสมอ แต่สำหรับคนที่ต้องดูแลระบบ Go ขนาดใหญ่ นี่คือของขวัญชิ้นใหญ่มาก
ถ้าใส่ให้เข้าที่อย่างถูกต้องไม่ได้ แปลว่ามันยังไม่ใช่สิ่งที่ถูกต้อง ก็กลับไปลองใหม่ และถ้ายังไม่ลงตัวอีก อาจหมายความว่าเรายังเข้าใจปัญหาไม่พอ จึงควรปล่อยให้สุกงอมต่อไปอีกนานหน่อย
ผมชอบมากที่ได้ทำงานกับคนที่คิดอย่างรอบคอบและพยายามให้ไอเดียที่ถูกต้องได้เข้าไป ขอบคุณที่ Russ ทำหน้าที่แบบ BDFL และขอบคุณที่ได้ทำงานกับ Ian, Rob, Rob ทั้งหมดนั้นทำให้ผมเป็นวิศวกรที่ดีขึ้นมาก
บทความที่เกี่ยวข้อง: Forward Compatibility and Toolchain Management in Go 1.21 - https://news.ycombinator.com/item?id=37122932
ประโยคที่ว่า “ความน่าเบื่อเป็นสิ่งที่ดี ความน่าเบื่อคือความเสถียร ความน่าเบื่อหมายความว่าคุณไม่ต้องสนใจว่าอะไรใน Go เปลี่ยนไป และโฟกัสกับงานของตัวเองได้” โดนใจมาก
ในงานหลักผมใช้ NodeJS และระบบนิเวศ JS โดยรวม ซึ่งเหนื่อยจริง ๆ ระบบนิเวศแตกเป็นเสี่ยง ๆ และทุกคนก็ทำตามวิธีของตัวเอง ทำให้ทำให้เสถียรได้ยาก ถึงอย่างนั้นผมก็ยังสนุกกับงานนี้อยู่ แต่ก็อยากให้ระบบนิเวศ JS มี ฐานรากสมัยใหม่ที่เสถียร ซึ่งพึ่งพาได้จริง ๆ
เครื่องมือและ API จำนวนมากเขียนด้วย Go และจุดที่ระบบเป้าหมายไม่ต้องมี runtime แยกต่างหากมักถูกยกเป็นข้อดีใหญ่ ภาษาเองก็ดูเรียนและใช้ง่าย การรองรับใน VSC กับ GoLand ก็ดี และข้อบ่นทั่วไปอย่างการจัดการ error ก็ดูไม่ใช่ข้อบกพร่องถึงตาย
ผมสงสัยว่า Go ยังต้องการอะไรอีกเพื่อจะกลายเป็นกระแสหลักของการพัฒนาไปอีกหลายสิบปีข้างหน้า หรืออย่างน้อยก็ครองส่วนใหญ่ของตลาดงาน เพราะในบางที่ก็ยังถูกมองว่าเป็นภาษาเฉพาะกลุ่มอยู่
ปัญหาคือการทำให้ทุกคนขยับขึ้นมาถึง baseline นี้ให้ได้ ถ้าไปถึงตรงนั้นได้ก็น่าจะดีขึ้นมาก
อีกอย่าง ระบบนิเวศ JS ครอบคลุมไปถึงงาน UI ฝั่ง frontend ด้วย และมีขอบเขตการใช้งานกว้างมาก จึงหลีกเลี่ยงไม่ได้ที่จะมี implementation หลายแบบ จริง ๆ แล้วบางทีก็เป็นเรื่องที่พึงประสงค์ด้วยซ้ำ
การย้ายโค้ดบางส่วนจาก Python ไป Golang ช่วยเรื่องการขยายระบบได้มาก เห็นข่าวว่า Go จะยังคงรักษาคำประกาศหลักเรื่อง backward compatibility ต่อไปแล้วดีใจจริง ๆ
พูดถึงการพาร์ส IP เหรอเนี่ย สงสัยจริง ๆ ว่าเดิมที
inet_ntoaของ BSD เขียนไว้ยังไงกันแน่sscanfที่ใช้atoi/atolกับ%d/%uจะพาร์สเป็นจำนวนเต็มฐาน 10 อย่างถูกต้องเสมอ ดังนั้นถ้าจะให้เกิดผลแปลก ๆ แบบนี้ ก็น่าจะต้องใช้%iหรือstrtouที่อิงฐาน 0inet_atonของผมระบุว่ามาจาก 4.3BSD: https://github.com/dank101/4.3BSD-Reno/blob/master/lib/libc/...inet_addrของ 4.2BSD ที่เก่ากว่านั้นก็ใช้ลอจิกเดียวกัน: https://github.com/dank101/4.2BSD/blob/master/lib/libc/inet/...inet_atonและinet_addrพาร์สที่อยู่ด้วยวิธีที่เป็นธรรมชาติที่สุดแล้ว ถ้าใช้ของอย่างstrtoulหรือโดยเฉพาะsscanfคงจะดูฝืน ๆ ความงามของพอยน์เตอร์ใน C คือมันทำให้งานพาร์สแบบง่าย ๆ ทำได้ง่ายมาก และบางทีอาจจะง่ายเกินไปด้วย/etc/hostsโดย เติม 0 นำหน้า ให้แต่ละอ็อกเท็ตสุดท้ายก็ต้องย้อนกลับไปลบ 0 ออก
ในฐานะผู้ออกแบบภาษา ผมเคารพการตัดสินใจต่าง ๆ ที่ทำไว้ตรงนี้ รวมถึงการตัดสินใจที่จะไม่ทำ Go 2 ตัวจริง
ผมเองก็คิดว่าจะนำ เทคนิคต่าง ๆ สำหรับรับประกันความเข้ากันได้ไปใช้เหมือนกัน