2 คะแนน โดย GN⁺ 2024-11-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใน net/http ของโค้ดเบส Go มีคอมเมนต์ระบุว่า สตริงข้อผิดพลาด "http: request body too large" ที่ MaxBytesError.Error() ส่งคืน ไม่สามารถเปลี่ยนได้เพราะ Hyrum's Law
  • Hyrum's Law คือหลักการที่ว่า เมื่อมีผู้ใช้ API มากพอ จะมีใครสักคนพึ่งพาพฤติกรรมที่สังเกตได้ แม้ไม่ได้อยู่ในสัญญาอย่างเป็นทางการก็ตาม
  • แม้แต่สตริงที่ดูเล็กน้อยอย่างข้อความแสดงข้อผิดพลาด หากโค้ดภายนอกทำงานโดยยึดกับถ้อยคำที่ตรงเป๊ะ การเปลี่ยนแปลงก็อาจทำให้โค้ดเดิมพังได้ทันที
  • ภายใน Go เองยังมีคอมเมนต์ลักษณะคล้ายกันใน crypto/rsa และ internal/weak โดยกล่าวถึงความเสี่ยงที่พฤติกรรมของสตรีมสุ่มหรือความหมายเชิง semantics ที่ยังไม่กำหนดชัดจะถูกตรึงไว้
  • เนื่องจากปัญหานี้ไม่ได้จำกัดอยู่แค่ Go, API หรือไลบรารีสาธารณะจึงควรออกแบบไม่ให้ พฤติกรรมที่ไม่ได้ตั้งใจ กลายเป็นมาตรฐานโดยพฤตินัย

Hyrum's Law ที่พบในโค้ด Go

  • MaxBytesError.Error() ใน net/http/request.go ส่งคืนสตริงต่อไปนี้
    • "http: request body too large"
    • ในคอมเมนต์ดังกล่าวเขียนไว้ว่า “Due to Hyrum's law, this text cannot be changed.”
  • Hyrum's Law เป็นหลักการที่ตั้งชื่อตาม Hyrum Wright และคำจำกัดความจาก hyrumslaw.com คือดังนี้
    • เมื่อมีผู้ใช้ API มากพอ ไม่ว่าสัญญาจะให้คำมั่นอะไรไว้ก็ตาม จะมีใครสักคนพึ่งพาพฤติกรรมที่สังเกตได้ทั้งหมดของระบบ
  • แก่นของกรณี MaxBytesError คือถ้อยคำที่ตรงเป๊ะของข้อความข้อผิดพลาดอาจถูกใช้ในโค้ดภายนอก
    • การเปลี่ยนถ้อยคำเพียงเล็กน้อยก็อาจทำให้โค้ดเดิมพังได้
    • จากผลค้นหา http: request body too large พบโค้ดโอเพนซอร์ส Go ที่ใช้สตริงดังกล่าว

กรณีในแพ็กเกจ Go อื่นและโค้ดเบสภายนอก

  • การพึ่งพาสตรีมสุ่มของ crypto/rsa

    • EncryptOAEP ใน crypto/rsa/rsa.go มีคอมเมนต์เกี่ยวกับ Hyrum's Law
    • ฟังก์ชันนี้ไม่ได้ให้คำมั่นว่าจะรันแบบ deterministic สำหรับสตรีมสุ่ม แต่เนื่องจากไม่ได้ใช้ MaybeReadByte จึงมีความเป็นไปได้ที่ใครบางคนจะพึ่งพาพฤติกรรมปัจจุบัน
    • SignPSS ใน crypto/rsa/pss.go ก็มีคอมเมนต์ในบริบทเดียวกัน
    • ทั้งสองกรณีถูกมองว่าเป็นคำมั่นที่ยอมรับได้ เพราะมี ไบต์สุ่ม จำนวนที่นิยามไว้อย่างชัดเจนถูกรวมเข้าไปใน ciphertext หรือ signature ด้วยวิธีที่นิยามไว้อย่างชัดเจน
  • ความเสี่ยงที่ semantics ของ internal/weak จะถูกตรึงไว้

    • internal/weak ระบุว่า toolchain ห้ามอย่างชัดเจนไม่ให้เข้าถึงแพ็กเกจนี้และฟังก์ชันอ้างอิงผ่าน go:linkname
    • semantics ของแพ็กเกจนี้ยังไม่ได้ผ่านกระบวนการเสนอ และหากเปิดเผยฟังก์ชันออกไป semantics เดิมอาจถูกตรึงไว้เพราะ Hyrum's Law
  • รูปแบบที่เกิดซ้ำแม้นอก Go

    • การกล่าวถึง Hyrum's Law ไม่ได้จำกัดอยู่เฉพาะ Go
    • สามารถดูกรณีจากหลายภาษาได้ใน ผลค้นหาหลายภาษา ของ grep.app
    • urllib.parse ของ Python และ array.h ของ Pixar OpenUSD ก็เป็นตัวอย่างโค้ดเบสที่เกี่ยวข้อง
    • วิวัฒนาการของ JavaScript ก็เชื่อมโยงกับกรณีที่การพึ่งพาพฤติกรรมแปลก ๆ และไม่ได้ตั้งใจจำนวนมาก กลายเป็นมาตรฐานโดยพฤตินัย

สิ่งที่ควรตรวจสอบก่อนเปลี่ยนแปลง

  • เมื่อเปลี่ยนโค้ด ควรพิจารณาไม่เฉพาะ API ที่มีเอกสารกำกับ แต่รวมถึง พฤติกรรมที่สังเกตได้ ซึ่งโค้ดภายนอกอาจพึ่งพาด้วย
  • จำเป็นต้องออกแบบระบบตั้งแต่แรกเพื่อลดความเป็นไปได้ที่ผู้ใช้จะพึ่งพาพฤติกรรมที่ไม่ได้ตั้งใจ

1 ความคิดเห็น

 
GN⁺ 2024-11-23
ความคิดเห็นบน Hacker News
  • กฎของ Hyrum เป็นข้อสังเกตที่มีประโยชน์ แต่ไม่ควรยึดติดจนสรุปอะไรผิด ๆ
    เวลารวมในการทำงานของฟังก์ชันก็เป็นคุณสมบัติที่สังเกตได้ ดังนั้นแม้แต่การปรับแต่งให้ฟังก์ชันเร็วขึ้นก็อาจมองได้ว่าเป็นการเปลี่ยนแปลงที่ทำให้พังได้ เพราะจู่ ๆ คิวอาจถูกล้างเร็วเกินไปจนเกิด deadlock ถึงอย่างนั้น ผู้ใช้ 99.99999999% ก็น่าจะชอบที่โค้ดเร็วขึ้นโดยไม่ต้องทำอะไรเลย
    ท้ายที่สุดแล้ว อะไรคือ การเปลี่ยนแปลงที่ทำให้พัง ย่อมเป็นสัญญาทางสังคมมากกว่าสัญญาทางเทคนิค ไม่อย่างนั้นก็จะกลายเป็นว่าเปลี่ยนอะไรไม่ได้เลยจริง ๆ ผู้เขียนไลบรารีควรระบุให้ชัดใน API ว่าอะไรบ้างที่จะไม่เปลี่ยน ประพฤติตัวอย่างมีเหตุผล และเข้าอกเข้าใจผู้ใช้ ส่วนผู้ใช้ไลบรารีก็ควรเข้าใจว่าการเอาอินเทอร์เฟซที่ไม่ได้มีการระบุไว้เป็นการพึ่งพาหลักนั้นเป็นความรับผิดชอบของตัวเอง และควรเข้าอกเข้าใจผู้เขียนด้วย

    • สำหรับผู้เขียนไลบรารีโอเพนซอร์ส ผมคิดว่าที่พูดมาทั้งหมดถูกต้อง
      แต่ถ้ามองอีกมุมหนึ่ง กฎของ Hyrum ไม่ใช่ทั้งสัญญาทางเทคนิคหรือสัญญาทางสังคม มันคือ คุณสมบัติทางเทคนิคเชิงเกิดใหม่ ที่ปรากฏในระบบซึ่งมีการใช้งานมากพอ
      จะรับมือกับคุณสมบัตินี้อย่างไรขึ้นอยู่กับบริบททางสังคม ถ้าเป็นผู้ดูแล FOSS ก็จะปล่อยการปรับแต่งที่ทำให้ 99.99% เร็วขึ้น และมีเพียง 0.01% ที่ต้องแก้โค้ดหรือย้ายไปใช้ API ใหม่ ถ้าเป็นบริษัทเทคขนาดใหญ่ ก็ต้องทั้งปรับแต่งและต้องไม่ให้มีอะไรพังเลยแม้แต่ 0% ภายในบริษัท จึงต้องทำงานร่วมกับหลายทีมเพื่อหาจุดประนีประนอม ถ้าเป็นบริษัทซอฟต์แวร์องค์กร ต่อให้พังแค่ 0.1% แต่ถ้าผู้ใช้นั้นเป็นหนึ่งใน 5 สัญญาลูกค้าใหญ่สุด ก็จะไม่ปล่อย
    • ครั้งหนึ่งผมเคยลดเวลาของรูทีนที่ไม่มีประสิทธิภาพมากจากประมาณ 100 วินาทีเหลือ 0.1 วินาที แล้วระบบรายงานก็พังเพราะเรื่องนั้น
      เพราะคนเขียนเดิมเรียกฟังก์ชันอะซิงก์หลายตัวไว้ แล้วตั้งสมมติฐานว่าเมื่อรูทีนที่ช้าในอดีตตัวนั้นจบลง ฟังก์ชันเหล่านั้นก็คงเสร็จหมดแล้ว ใช้เวลานานมากกว่าจะหาว่าเกิดอะไรขึ้นกันแน่
    • ในยุค 1980 มีปัญหาแบบนั้นจริง ๆ
      เพราะงั้น PC จึงมี ปุ่มเทอร์โบ สำหรับลดความเร็ว และคอมพิวเตอร์ 8 บิตก็ไม่เพิ่มความเร็วอยู่นานถึง 10 ปี ทั้งที่มี CPU ที่เร็วกว่ามากแล้ว ทุกวันนี้แทบทุกอย่างทำงานบน CPU มากกว่าหนึ่งตัว ดังนั้นนอกจากกรณีที่แค่ต้องเร็วพอแล้ว แทบไม่มีใครพึ่งพาเวลาทำงานของฟังก์ชันอีก แม้แต่ในงาน embedded ก็พยายามหลีกเลี่ยงการพึ่งพาแบบนั้น หลังจากเคยเจอกรณี CPU เดี่ยวเลิกผลิต
    • สักวันหนึ่งผมอยากลองทำ lightning talk เกี่ยวกับ load bearing teapot
      ว่าทำไมถึงทำให้ HTTP Status 418 กลายเป็นการพึ่งพาหลักใน API ภายใน และภายใต้ข้อจำกัดที่มีอยู่ ทำไมมันถึงเป็นตัวเลือกที่แย่น้อยที่สุด
    • เรื่องอย่างเวลารวมในการทำงานของฟังก์ชันไม่ใช่สิ่งที่อยู่ในการควบคุมของคนเขียนฟังก์ชัน ดังนั้นตรรกะนี้จึงดูเกือบจะไร้สาระ
      สภาพแวดล้อมที่รัน ภาระงานของระบบในขณะนั้น หรือการทำงานของ GC ล้วนมีผลได้ทั้งนั้น
      สรุปคือ ผมไม่มองว่า พฤติกรรมเชิงเกิดใหม่ ที่เกิดจากเครื่องเป็นอินเทอร์เฟซที่ตั้งใจไว้หรือเป็นสัญญาในรูปแบบใด ๆ ดังนั้นถึงจะมีใครพึ่งพาพฤติกรรมที่ไม่ได้ตั้งใจไว้ ผมก็ไม่มองว่านี่เป็นการเปลี่ยนแปลงที่ทำให้พัง เหมือนกับที่การแก้บั๊กจุกจิกไม่ถือเป็นการเปลี่ยนแปลงที่ทำให้พัง
      ในกรณีนี้ ดูจะเป็นหลักฐานมากกว่าว่า Go ทุ่มเทอย่างมากกับการรักษาความเข้ากันได้ย้อนหลัง
  • ฮ่าๆ คอมเมนต์ใน crypto/rsa นั่นฉันเป็นคนเขียนเอง ใน Go เขาให้ความสำคัญกับ กฎของ Hyrum และการคงความเข้ากันได้ย้อนหลัง https://go.dev/doc/go1compat อย่างจริงจังมาก
    ตัวอย่างเช่น ในฟังก์ชัน GenerateKey หลายตัว จะมีการอ่านเพิ่มอีก 1 ไบต์จากสตรีมสุ่มด้วย MaybeReadByte https://pkg.go.dev/crypto/internal/randutil#MaybeReadByte เพื่อไม่ให้อัลกอริทึมถูกตรึงตายตัว เพิ่งเมื่อวานนี้ก็มีรายงานเข้ามาว่า private ECDSA key ที่มี public key เป็น nil เคยใช้ได้ในอดีตแต่ตอนนี้ใช้ไม่ได้แล้ว ซึ่งคงต้องทำให้กลับมาใช้ได้อีกครั้ง https://go.dev/issue/70468
    การวนลูปแมปใช้ลำดับแบบสุ่มเพื่อไม่ให้รายละเอียดการทำงานภายในรั่วออกมา เอาต์พุตของ rand.Rand ถือเป็นส่วนหนึ่งของสัญญาความเข้ากันได้ จึงต้องทุ่มความพยายามพอสมควรกว่าจะปรับปรุงได้ https://go.dev/blog/randv2 https://go.dev/blog/chacha8rand
    เราถกกันเสมอว่าควรเขียนคำรับประกันอะไรไว้ในเอกสาร และควรระบุพฤติกรรมไหนว่า “อาจเปลี่ยนได้” เพราะรู้ดีว่าสิ่งที่เอกสารระบุไว้แล้วจะเปลี่ยนไม่ได้เด็ดขาด และแม้แต่สิ่งที่ไม่ได้ระบุว่า “อาจเปลี่ยนได้” ก็มักจะเปลี่ยนได้ยากอยู่ดี https://go-review.googlesource.com/c/go/+/598336/comment/5d6...

    • การเปลี่ยน ลำดับการวนลูปแมป ช่วยลด breaking change ในอนาคตได้ เพราะทำให้คนไม่สามารถพึ่งพาลำดับแบบใดแบบหนึ่งได้ แต่ในตอนที่เปลี่ยนจริง มันก็เป็น breaking change สำหรับโค้ดที่เคยพึ่งพาลำดับเดิม
      ถึงอย่างนั้นก็ยังมองว่าเป็นการแลกเปลี่ยนที่คุ้มค่า ฉันใช้ Go เยอะและชอบการคงความเข้ากันได้ย้อนหลังแบบเข้มแข็ง แต่ถ้าการเพิ่มอิสระให้ทีม Go ปรับปรุงประสิทธิภาพและเพิ่มฟีเจอร์ได้มากขึ้น ต้องแลกกับสัดส่วน breaking change ที่สูงขึ้นเล็กน้อย ฉันก็ยินดีรับได้
      พอมองไปที่นรกที่ผู้ใช้ใน ecosystem อื่นต้องเผชิญ อย่างเช่น Python ก็คิดว่านี่ไม่ใช่มุมมองของฉันคนเดียว
    • บอกว่าใช้ MaybeReadByte ในฟังก์ชัน GenerateKey หลายตัวก็จริง แต่ดูเหมือน ed25519 จะไม่ได้ทำแบบนั้น
      ก่อนจะมี ed25519.NewKeyFromSeed() วิธีเดียวที่จะได้ public Ed25519 key จาก private key ก็คือวิธีนี้ และแทบจะแน่นอนว่าฉันเคยเขียนโค้ดที่พึ่งพาพฤติกรรมนี้ แม้จะไม่ค่อยชอบนัก แต่ตอนนั้นมันเป็นทางเลือกเดียว เลยจำได้ง่าย
      อย่างไรก็ดี เป็นเรื่องดีที่เอกสารของ ed25519.GenerateKey ระบุชัดว่าเอาต์พุตเป็นแบบกำหนดได้แน่นอน ดูเหมือน Go จะทำได้ดีมากในการตรวจสอบและรักษาพฤติกรรมที่ตกผลึกไปแล้วใน API ด้านคริปโต พร้อมกับป้องกันไม่ให้มีพฤติกรรมใหม่ๆ ตกผลึกเพิ่ม
    • กรณี nil key ทำให้สงสัยว่าการรองรับอะไรแบบนี้ยังถือว่าสมเหตุสมผลอยู่แค่ไหน
      มันจะกลายเป็นเหมือน A20 line อันเลื่องชื่อ (https://en.wikipedia.org/wiki/A20_line) ที่ต้องแบกพฤติกรรมพังๆ นี้ไปตลอดกาล
    • น่าขันตรงที่เมื่อก่อนฉันเคยเขียน load balancer ด้วย Go และดันพึ่งพาลำดับการวนลูปแมปแบบสุ่ม
    • นี่เป็นหนึ่งในสิ่งที่ถูกประเมินค่าต่ำที่สุดของ Go โค้ดที่เขียนเมื่อ 12 ปีก่อน ยังทำงานได้เฉยๆ จนถึงตอนนี้
  • วิธีแก้ปัญหาที่ถูกยกมาแบบเฉพาะเจาะจงคืออย่าใช้อะไรแบบ string-based error แต่ให้ใช้ sentinel error https://thomas-guettler.de/go/wrapping-and-sentinel-errors
    ถ้ามองกว้างกว่านั้น ก็คือไม่ควรสร้างโค้ดที่ทำให้ผู้ใช้ API อยากพึ่งพาสตริงที่ไม่ใช่องค์ประกอบเชิงเทคนิคของภาษาแม้แต่น้อย หากใช้สิ่งที่เป็น first-class ของภาษา เช่น predefined error value, type หรือค่าคงที่ที่เก็บสตริงที่ไม่ใช่เชิงเทคนิค ผู้ใช้ API ก็จะเทียบค่าที่คืนมากับค่าคงที่ได้ แทนที่จะต้อง hardcode สตริงเอง
    กฎของ Hyrum มีอยู่จริงแน่นอน แต่เราลดผลกระทบของมันได้

    • เรื่องที่น่าหงุดหงิดคือ error ที่เป็นปัญหานั้นเป็น sentinel error อยู่แล้ว
      Grafana ซึ่งดูจากการค้นหาที่ลิงก์มาแล้วน่าจะเป็นต้นเหตุระดับบนสุด ควรใช้ errors.As(&http.MaxBytesError{}) แทนการเปรียบเทียบสตริง
      ใจความสำคัญของกฎของ Hyrum คือ ต่อให้ออกแบบ API มาดีแค่ไหนก็ไม่ได้ช่วยอะไร ผู้คนจะพึ่งพาพฤติกรรม ไม่ใช่สัญญา
    • ในตัวอย่างนี้ ความรับผิดชอบอยู่ที่ผู้ใช้ ไม่ใช่ผู้ให้บริการ
      คุณยังสามารถเขียนโค้ดแบบ err.String() == "no more tea available." ได้อยู่ ฉันเห็นด้วยว่าไม่ควรทำแบบนั้น แต่ก็ไม่มีอะไรป้องกันไม่ให้ทำ
      แถม errors.Is ก็เพิ่งถูกเพิ่มเข้ามาใน Go ได้ไม่นานนัก ดังนั้นในช่วงที่คนยังตรวจ error กันแบบนี้อยู่ การเช็กสตริงล้วนๆ จึงเป็นวิธีที่ง่ายกว่า ใน Go ผู้ให้บริการ API ไม่สามารถห้ามผู้ใช้ตรวจค่า .String() ที่คืนมาได้
    • เมื่อหลายปีก่อน การเปรียบเทียบ error แบบสตริง เป็นวิธีเดียวที่จะทำแบบนี้ได้ และ Go ก็มีคำมั่นเรื่องความเข้ากันได้ย้อนหลัง
    • โค้ดที่ตรวจสอบ raw error string นั้นเป็นโค้ดที่แย่ล้วนๆ และควรถูกตัดออกจากการรับประกันความเข้ากันได้ย้อนหลังของ Go
      โดยเฉพาะใน standard library แทบไม่มีข้อแก้ตัวเลย
    • ปัญหาอยู่ที่การออกแบบช่วงแรกของ Go เป็นเวลานานที่ string-based error เป็นทางเลือกเดียว และถ้าจำไม่ผิด ทุกวันนี้ก็ยังเหลืออยู่ในบางแพ็กเกจของ standard library ไม่ต้องพูดถึงทั้ง ecosystem
      นี่แหละคือสิ่งที่เกิดขึ้นเมื่อคุณจงใจไม่สนใจประวัติศาสตร์ของภาษาโปรแกรม แล้วเลือกแนวทาง “ค่อยออกแบบไปพร้อมกับการสร้าง”
  • วิธีรับมือกับกฎของ Hyrum ก็เป็นหัวข้อที่น่าสนใจเช่นกัน
    ความเป็นไปได้อย่างหนึ่งคือใส่ ความสุ่ม ลงไปในส่วนที่เราไม่อยากให้คนพึ่งพา
    ถ้าจำไม่ผิด โปรโตคอล QUIC ก็ทำแบบนี้ มีฟิลด์ที่ไม่ได้ใช้ในเวอร์ชันปัจจุบันอยู่ แต่เพื่อไม่ให้เราเตอร์เริ่มใช้ฟิลด์นั้นในการระบุแพ็กเก็ต สเปกจึงกำหนดให้ตั้งค่าเป็นค่าสุ่มแทนที่จะเป็น null byte
    แหล่งอ้างอิงน่าจะอยู่ที่นี่: https://www.rfc-editor.org/rfc/rfc9000#section-17.2.1
    “ค่าของฟิลด์ Unused จะถูกตั้งโดยเซิร์ฟเวอร์เป็นค่าใดก็ได้แบบสุ่ม ไคลเอนต์ต้องเพิกเฉยต่อค่าของฟิลด์นี้เสมอ [...] โปรดทราบว่า QUIC เวอร์ชันอื่นอาจไม่มีคำแนะนำแบบเดียวกันนี้”
    เข้าใจว่าสิ่งนี้เรียกว่า greasing และมีไว้เพื่อป้องกัน ossification

    • GREASE เป็นตัวย่อที่สร้างขึ้นใน RFC 8701 ย่อมาจาก “Generate Random Extensions And Sustain Extensibility” และเดิมใช้ในบริบทของ TLS
      https://www.rfc-editor.org/rfc/rfc8701.html
      ร่างแรกสุดของ RFC นี้ย้อนกลับไปได้ถึงกลางปี 2016 และน่าจะเป็นช่วงที่คำนี้ปรากฏต่อสาธารณะครั้งแรก: https://datatracker.ietf.org/doc/html/draft-davidben-tls-gre...
    • เยี่ยมมาก ฉันค่อนข้างคุ้นกับ QUIC แต่ไม่เคยรู้เรื่องนี้มาก่อน
      ไม่มีอะไรแย่ไปกว่าการตื่นขึ้นมาอีก 10 ปีให้หลังแล้วพบว่าบิตพวกนั้นจำเป็นจริง ๆ แต่เราเตอร์ 20 รุ่นจาก 10 แบรนด์ดันตัดสินไปแล้วว่าบิตเหล่านั้นต้องมีรูปแบบเฉพาะเท่านั้น
      ถ้าฝั่งตรงข้ามมี checksum หรือการเข้ารหัสอยู่ด้วย จนบิตเสียหายแล้วทุกอย่างพัง ก็ถือว่าได้คะแนนเพิ่ม พวก “แฮ็กอัจฉริยะ” ของ middlebox นี่ปวดหัวจริง ๆ
  • นี่เป็นตัวอย่างที่ดีของซอฟต์แวร์แบบ stringly typed
    ผู้ออกแบบ Go ไม่ต้องการ exception แต่ก็ยังมีสิ่งคล้ายกันผ่าน panic/recover และ error ที่ไม่มี type ก็เป็นโทษ ในทางกลับกันจะจัดการกับ error ที่มี type โดยไม่มี pattern matching ได้อย่างไร? catch ของภาษาส่วนใหญ่ก็เป็น pattern matching แบบพื้นฐานอยู่แล้ว
    https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

    • Go มี typed errors อยู่แล้ว แค่กรณีนี้ไม่ได้ใช้เท่านั้น
  • เมื่อก่อนที่ที่ทำงานเก่า ฉันเคยเจอ คำสะกดผิด ในข้อความ error แล้วแก้ไขมัน แต่ต่อมาก็พบว่าเครือข่ายการพึ่งพาข้อความสะกดผิดนั้นลึกมากจนแทบแก้จริงไม่ได้ สุดท้ายเลยต้องย้อนกลับไปใช้ข้อความที่สะกดผิดเหมือนเดิม
    จนถึงตอนนี้ก็ยังคาใจอยู่

    • หรือว่าคุณคือ Phillip Hallam-Baker? :)
      https://en.wikipedia.org/wiki/HTTP_referer
    • อย่างน้อยในบริบทของโพสต์นี้ ก็ยังพอมีเวลาแก้ Golang อยู่
  • นี่ก็เป็น Hyrum's Law แบบหนึ่ง แต่ในทางปฏิบัติก็คือ ความเป็น Go แบบ Go
    ถ้า error เป็น enum type ฝั่งผู้ใช้ก็อาจเปลี่ยนได้ด้วยการแทนที่สตริงอย่างเดียว แต่เมื่อใช้สตริงราวกับเป็น type ก็จะไม่มีทางรู้ว่าผู้ใช้พึ่งพามันอย่างไรอยู่บ้าง แค่ไปตรวจ 6 ตัวอักษรกลางข้อความ error แล้วเปลี่ยน ก็อาจพังได้
    ทั้งที่ภาษอื่น ๆ ใช้ทางเลือกที่ดีกว่ามานานหลายสิบปีแล้ว แต่นี่ก็เป็นอีกหนึ่งการตัดสินใจออกแบบที่น่ากลัวและล้าสมัย ความผิดพลาดช่วงต้นที่แก้ไม่ได้ในภายหลังจะผูกมัดไปตลอดกาล

    • น่าเสียดายที่คอมเมนต์นั้นผิดโดยพื้นฐาน ในหลายกรณี ตัวสตริงเองคือ API อย่างเป็นทางการ
  • น่าสนใจที่กฎนี้ตรงข้ามกับ หลักความทนทาน หรือกฎของ Postel อย่างพอดี
    “เข้มงวดเมื่อส่ง ใจกว้างเมื่อรับ”
    ถ้าคุณรับอินพุตอย่างใจกว้าง คุณต้องเข้าใจว่าตัวเองใจกว้างในรูปแบบไหน และอย่างน้อยก็บันทึกไว้ภายในองค์กร เพราะด้วย Hyrum's Law หลังจากเปลี่ยนโค้ดเบสขนาดใหญ่แล้ว คุณจะต้องรองรับวิธีเหล่านั้นทั้งหมดตลอดไป
    นั่นจึงเป็นเหตุผลที่ฉันพยายามไม่สร้าง API ที่ “ใจกว้างกับสิ่งที่รับเข้า”

    • ฉันก็ชอบแนวทางนั้นเหมือนกัน
      ถ้าคุณตั้งเกณฑ์ข้อมูลที่ API รับให้หลวม สุดท้ายคุณก็ต้องตัดสินใจว่าจะนวดข้อมูลนั้นให้กลายเป็น รูปแบบมาตรฐาน แบบไหน และการตัดสินใจนั้นก็มักลงเอยด้วยพฤติกรรมที่ผู้ใช้รู้สึกว่าแปลกใจในทางใดทางหนึ่งแทบทุกครั้ง
  • ดูเหมือนผู้เขียนแพ็กเกจแต่ละคนจะยอมรับปัญหานี้ไม่เท่ากัน ไม่กี่วันก่อนฉันเห็นคอมเมนต์นี้ในแพ็กเกจ json
    isValidNumber รายงานว่า s เป็น JSON number literal ที่ถูกต้องหรือไม่
    isValidNumber ควรเป็นรายละเอียด implementation ภายใน แต่แพ็กเกจที่ใช้งานแพร่หลายกลับเข้าถึงมันผ่าน linkname
    สมาชิกตัวเด่นใน hall of shame รวมถึง github.com/bytedance/sonic

  • นี่คือสิ่งที่เรียนรู้จากการปล่อย API
    ไคลเอนต์จะทำทุกอย่างที่จำเป็นเพื่อให้งานตัวเองเสร็จ แม้ว่าจะไม่ใช่วิธีที่ผู้เผยแพร่ตั้งใจไว้ก็ตาม ไคลเอนต์ไม่อ่านเอกสารประกอบ ถ้ามีไคลเอนต์มากพอพึ่งพาพฤติกรรมใดพฤติกรรมหนึ่ง บั๊กก็กลายเป็นส่วนหนึ่งของ API จำนวนครั้งที่เรียก API ไม่ได้สอดคล้องกับความสำคัญเสมอไป
    เพราะอย่างนั้นเวลาพัฒนา API ฉันจึงพยายามออกเบต้า API ให้เร็วที่สุดเพื่อดูว่าคนเอาไปใช้อย่างไรและลดเรื่องไม่คาดคิดลง โดยส่วนใหญ่ก็จะรองรับเวอร์ชันก่อนหน้าควบคู่กับการเพิ่ม major version ซึ่งจะทำแบบนั้นได้ก็ต้องนิยาม SLA ของ API ให้ชัดเจน