ใช้ OpenJDK ตัวไหนดี

ช่วงนี้ทำ build infra ให้ Java project แล้วก็รำคาญ base image เลยไปทำ image Java 16/17 ให้ทีม

ทีนี้ก็สงสัย ว่าเราใช้ OpenJDK ตัวไหนดี…

ถ้าตอบสั้นๆ คือถ้าไม่เลือก Oracle แล้วแทบจะเหมือนถามว่าอยากได้ Blockchain สีอะไร

ถ้าจะเอาคำตอบดีๆ เลื่อนไปล่างสุดอ่านได้เลยครับ

The contenders

ปัจจุบันเราสามารถโหลด OpenJDK 11 ได้จากที่ต่างๆ ดังนี้ (ข้อมูลจาก javaalmanac.io) ซึ่งต้องทำความเข้าใจว่าที่มาที่ไป ทำไมคนอยาก build JDK กัน

  • Eclipse Temurin โดยกลุ่ม Adoptium หรือชื่อเดิมก่อนมาอยู่ Eclipse คือ AdoptOpenJDK เป็นโครงการ build OpenJDK จากชุมชน มี IBM ดันหลังเล็กน้อย ถ้าอยากได้แบบไม่ติดแบรนด์คิดว่าเจ้านี้ดีที่สุด
  • AdoptOpenJDK Upstream build (ยังไม่มีย้ายไป Temurin) มาจากคำขอของทีม OpenJDK update project ที่อยากได้ build เพียวๆ ไม่แต่งเติมใดๆ ทั้งสิ้น แต่ Oracle ไม่ให้วางไว้ในเว็บ java.net
  • Amazon Corretto เนื่องจาก AWS ใช้ Java ด้านในค่อนข้างเยอะ ก็เลยแจกตัวที่ใช้งานมาเพื่อให้นำไปใช้งานได้ รวมถึงถ้ามี AWS support อยู่แล้วก็ใช้ได้เลย
  • Azul Zulu เป็นบริษัทจากอเมริกาที่ support Java มากว่า 20 ปี
  • Azul Prime เวอร์ชั่นเสียเงินที่มี optimization เพิ่มเติมเช่น garbage collector ตัวใหม่ไม่มีที่อื่น
  • BellSoft Liberica เป็นบริษัท support Java จากรัสเซีย
  • Microsoft Build of OpenJDK ทีแรกผมก็สงสัยว่า Microsoft ใช้ Java ทำอะไร ตัวเก่าก็ไม่เห็นจะดี แต่อันนี้เป็นซอร์สโค้ดเหมือนชาวบ้าน และ Microsoft เองก็ใช้ Java ใน Azure, LinkedIn และ Minecraft อยู่แล้ว ถ้ามันไม่ดีผมว่ากองทัพ Minecraft น่าจะพร้อมถล่ม
  • Oracle JDK รุ่นนี้ไม่ฟรีสำหรับการใช้งานใน production ต้องใช้รุ่น Java 17 เป็นต้นไปถึงจะฟรี
  • Red Hat ผู้ผลิตลินุกซ์อันดับต้นๆ แต่ก็มีรุ่น Windows พร้อมใช้พร้อมซื้อ commercial support ได้ด้วย แต่ยังไม่มีเวอร์ชั่นแมค
  • ojdkbuild เป็น Red Hat source code unofficial build ผมเข้าใจว่าน่าจะมีมาก่อนที่ Red Hat จะออกรุ่น Windows เอง
  • SapMachine เนื่องจาก SAP ก็ใช้ Java ใน product ก็เลยมี OpenJDK build ของตัวเองด้วย สามารถใช้ SAP support ได้ถ้าเป็นประเด็นที่เกี่ยวกับการใช้ใน SAP
  • Debian/Ubuntu จะมีใน apt-get ซึ่งโครงการ build เอง ไม่ได้โหลดจากเจ้าไหนข้างบนนี้มา

Source code

โครงการ OpenJDK เป็นโครงการ open source แปลว่า คุณจะได้แต่ source code กลับบ้านไป เวลา Java มี improvement อะไรมันก็จะไปโผล่ใน source code นี้ แต่มันจะไม่มี exe/app/deb/rpm ให้ดาวน์โหลด

ดังนั้นจะใช้ Java ยี่ห้อไหน มันก็ออกมาจาก source code เดียวกัน ก็ควรจะเหมือนกันเลือกอะไรก็ได้หรือเปล่า? ผมคิดว่า OpenJDK Release Map อธิบายไว้ค่อนข้างดีแต่มองภาพยากนิดหน่อย ก็สรุปได้ว่าแต่ละ vendor มี source code “on top” ของตัวเอง ซึ่ง vendor ส่วนมากที่เป็น open source จ๋าๆ จะพูดเหมือนกันหมดว่าเรา upstream first แปลว่าส่วนที่ทำเพิ่มแก้ไม่เยอะ ไม่ค่อยสำคัญ บางทีอาจจะเป็นโค้ดเดียวกันแต่ feature flag ต่างกันเฉยๆ ส่วน vendor ที่ไม่ใช่ open source ก็มีอยู่ 2 เจ้า ซึ่งชัดเจนมากว่ามี on top

  • Oracle บอกว่า Java version เก่า ไม่เล่นด้วยปล่อย community ทำกันเองซึ่งก็คือแทบทุก vendor ในนี้ยกเว้น Oracle แต่จะสังเกตว่า Oracle ก็ออก JDK เวอร์ชั่นเก่าเหมือนกันซึ่งโค้ดในนั้นไม่อยู่ใน open source อยากได้ต้องซื้อ และฝั่ง open source เองก็ไม่แน่ใจว่า Oracle ใช้โค้ดจากที่ community แก้กันด้วยหรือเปล่า
  • Azul Prime ซึ่งมีลูกเล่นเฉพาะตัว เช่น ReadyNow, LLVM-based JIT, Pauseless C4 GC

เจ้าอื่นๆ ที่พอจะทราบว่าแก้อะไรบ้างเช่น

  • Amazon Corretto 8 มี patch พอสมควร ส่วนใน 11 ก็มีแก้เล็กน้อยให้รองรับ Amazon Linux 2 และ 17 ยังไม่มีแก้
  • Red Hat ระบุความแตกต่างไว้พอสมควร หลักๆ คือเปิด Shenandoah GC (เป็น compile time feature flag ที่ vendor อื่นๆ จะเปิดหรือปิดก็ได้ เนื่องจากเป็นของ Red Hat), ใช้ library จาก Red Hat เองไม่ใช่ตัวที่ vendor มาใน source tree
  • Microsoft มีระบุ change ไว้เล็กน้อย หลักๆ คือเน้นรองรับ Windows/Mac on ARM

ถ้าต้องการการันตีว่า ไม่เอา on top ขอ OpenJDK “แท้ๆ” สามารถดาวน์โหลดได้จาก AdoptOpenJDK Upstream build (ขณะที่เขียนนี้ยังไม่ได้ย้ายไป Eclipse) หรือ jdk.java.net ซึ่งมีเฉพาะรุ่นล่าสุดและรุ่นที่ยังไม่ออกเท่านั้น

ปัญหาหนึ่งที่จะเจอกันทั้งหมดทุกเจ้าคือ security update ต่างๆ ซึ่งจำเป็นต้องให้ออกพร้อมๆ กัน โดยใน OpenJDK จะมีกลุ่ม Vulnerability Group ที่ vendor เจ้าต่างๆ สามารถเข้าถึง patch ลับสำหรับช่องโหว่ที่ยังไม่เปิดเผยสำหรับ test release ก่อนที่จะประกาศพร้อมกันตอนที่ patch เข้าไปใน OpenJDK หลัก (ส่วน security patch ที่ Oracle ทำเองจะ merge เข้าเลยไม่มีขั้นตอนนี้) ดังนั้นควรจะเลือก vendor ที่มีสิทธิ์ในกลุ่ม OpenJDK Vulnerability Group ด้วย

เท่าที่ผมลอง cross reference จาก census ว่าทีมนี้ปัจจุบันทำงานที่ไหนบ้าง ก็จะมีคนจาก Amazon, SAP, Azul, Red Hat, BellSoft, SUSE, IBM, Canonical (Ubuntu) อยู่ในกลุ่มนี้ จากในแผนภาพระบุว่า AdoptOpenJDK และ Debian ไม่มีข้อมูล security update ล่วงหน้าจากกลุ่มนี้ (ข้อมูลจาก 2019)

TCK

ประเด็นถัดมาคือ Java Technology Compatibility Kit ซึ่งการันตีว่า JVM ที่ใช้ผ่านมาตรฐานรับรองว่าเป็น Java ได้ โดย TCK นี้ต้อง license มาจาก Oracle ไม่มีแจกฟรี

เท่ามีข้อมูลตอนนี้ OpenJDK ทุกเจ้าที่มีใช้งานผ่านการตรวจสอบ TCK กันหมดแล้ว ที่จะไม่มีข้อมูลว่าผ่านหรือไม่ก็คือตัวที่อยู่ใน repo ของ Debian/Ubuntu และตัวที่ยังไม่ผ่านคือ ojdkbuild และ Microsoft

Packaging & Build Matrix

ส่วนที่ผมคิดว่าแต่ละเจ้าจะแตกต่างกันเยอะคือ packaging เพราะถ้าโหลดจาก jdk.java.net จะได้ zip file เอาไปใช้งานลำบาก และมีแค่ Windows/Mac/Linux 64 bit, Mac/Linux ARM 64 bit เท่านั้น ดังนั้นควรจะพิจารณาด้วยว่าเจ้านั้นๆ รองรับ OS/Architecture ที่ใช้งานหรือไม่ และรองรับ package management ที่ใช้งาน (Docker/Chocolatey/Homebrew/Yum/APT/SDKMAN)

ส่วนมากแล้วเท่าที่เห็นแทบทุกเจ้ามี combination พื้นฐานกันเกือบครบ คือ Windows/Mac/Linux 64 bit, Docker ทั้ง Red Hat/Debian และ Chocolatey/Homebrew/Yum/APT/SDKMAN ก็จะมีเฉพาะ combination ยากๆ คือ

  • Alpine Linux รองรับเฉพาะ Azul, BellSoft, Amazon
  • Mac ARM รองรับเฉพาะ Azul, BellSoft, Microsoft
  • 32 bit OS ตอนนี้มีเฉพาะ Temurin (Windows only), Azul, BellSoft

Support

ถ้าจะซื้อ support แล้ว ผมยังคิดว่าซื้อกับ Oracle น่าจะดีที่สุดและช่วยสนับสนุนการพัฒนา Java ด้วย แต่ถ้าไม่ชอบ Oracle แล้ว vendor แทบทุกเจ้ามี commercial support ทั้งหมด

  • Eclipse Temurin ซื้อได้จาก IBM หรือ Azul
  • SapMachine ใช้ SAP support ได้เฉพาะเรื่องที่เกี่ยวกับการใช้งานใน SAP
  • Microsoft support ใช้ได้เฉพาะการใช้งานใน Azure
  • Amazon ใช้ AWS support เค้าไม่ได้จำกัดว่าต้องใช้ใน AWS เท่านั้นแต่ก็ไม่แนะนำให้ซื้อ AWS support ถ้าไม่ได้ใช้งาน AWS
  • Azul กับ BellSoft เป็นบริษัท support Java โดยเฉพาะอยู่แล้ว

นอกจากเรื่องคุณภาพของ support ที่ได้รับแล้ว ผมยังคิดว่า contribution ที่บริษัทที่เราซื้อส่งกลับให้ OpenJDK ควรจะเป็นปัจจัยที่นำมาคิดด้วย รูปหนึ่งที่พอจะตอบได้คือจำนวน issue ที่ปิดโดยแต่ละองค์กรใน JDK 16 release

ก็จะเห็นว่า top 10 ไม่รวม Oracle ได้แก่

  1. Red Hat
  2. SAP
  3. Tencent
  4. นักพัฒนาอิสระ
  5. ARM
  6. Amazon
  7. Bellsoft
  8. NTT Data
  9. Microsoft
  10. Azul

ทั้งนี้ Azul ก็มีคอมเมนต์ว่าตรงนี้ก็เป็นแค่จำนวนแต่ไม่เน้นถึง impact หรืองาน maintenance เวอร์ชั่นเก่า

JRE

สมัยก่อน Java จะมี installer 2 ตัวคือ Java Development Kit (JDK) กับ Java Runtime Environment (JRE) ซึ่งตัวแรกจะมี compiler ด้วย แต่ตั้งแต่ Java 11 แล้ว Java แนะนำให้ใช้ jlink สร้าง custom runtime ที่มีเฉพาะส่วนที่แอพใช้งานเท่านั้นแล้วผูกไปพร้อมกับแอพเลย จึงไม่มี JRE ให้ดาวน์โหลดอีก

แต่ปัจจุบันก็ยังมีบาง vendor ที่ยังมี JRE อยู่ ผมก็ไม่แน่ใจว่าเค้าเอามาจากไหน ก็ได้แก่ Temurin, Azul, BellSoft, Red Hat, SAP ที่จะไม่มีก็คือ Oracle, Amazon, Microsoft

JDK นอกกระแส

จริงๆ แล้ว list JDK ยังไม่หมดแค่นั้นแต่มีตัวอื่นๆ อีก ถ้าอยากลองของแปลกก็ใช้งานได้

  • Jetbrains Runtime คือตัวที่ใช้รันโปรแกรมของ Jetbrains (เช่น IntelliJ) สามารถโหลดมาใช้เดี่ยวๆ ได้ มีจุดเด่นคือปรับแต่งให้ใช้งาน desktop application ได้ดี แต่ไม่ได้รับรอง TCK
  • Alibaba Dragonwell ที่ Alibaba ใช้รันเว็บภายใน มีฟีเจอร์เฉพาะตัวคือ JWarmup ที่ใช้บันทึกข้อมูล JIT แล้วโหลดภายหลังเพื่อให้ได้ประสิทธิ์ภาพทันทีหลังจาก cold boot, Wisp2 เป็น coroutine สำหรับ Java thread ฟีเจอร์เหล่านี้เพิ่งตั้งไข่ใน OpenJDK คงอีกหลายปีอาจจะได้ใช้งาน แต่สามารถใช้ได้วันนี้เลยใน Dragonwell
  • Tencent Kona เป็นตัวที่ Tencent ใช้ ซึ่งมีปรับจูน GC บ้าง รองรับเฉพาะลินุกซ์เท่านั้น
  • Huawei Bisheng JDK ทาง Huawei บอกว่าเค้าใช้ตัวนี้ รองรับเฉพาะ Linux ARM เท่านั้น และมีการปรับแต่งหลายๆ จุดซึ่งมีเอกสารเฉพาะภาษาจีน ผมคิดว่าน่าจะเน้นไปทาง ARM เนื่องจาก Huawei มี ARM CPU ของตัวเอง

คนหนึ่งที่หายไปไม่ว่าจาก list นี้หรือด้านบนคือ Google ซึ่งผมคิดว่าเค้าไม่ว่างทำ โดน Oracle ฟ้องอยู่ เท่าที่หาข้อมูลมา App Engine v2 ใช้ Ubuntu openjdk-11-jdk ส่วน Bazel rules_java ใช้ Azul

ใช้อะไรดี

ถ้าอ่านทั้งหมดนี้แล้วตัดสินใจไม่ได้สักที ในความเห็นของผมคิดว่า

Server

พิจารณาตามลำดับนี้ครับ (นึกว่าเป็น flow chart ก็ได้)

  1. ถ้ามี license Red Hat อยู่แล้ว (ที่ไม่ใช่ CentOS หรือ rebuild อื่นๆ) ก็ yum มาลงได้เลย
  2. ถ้ามี support จาก AWS, SAP, Azure อยู่แล้วและ workload รันบนนั้น ก็ใช้ของยี่ห้อนั้นได้
  3. ถ้าใช้ Alpine Linux มีตัวเลือกคือ AWS กับ Azul
  4. ถ้าจำเป็นต้องซื้อ support คิดว่าซื้อจาก Oracle ได้ก็ดี (ผมไม่เคยซื้อกับ Oracle แนะนำไม่ได้)
  5. ถ้าไม่ชอบ Oracle ใช้ Azul
  6. ถ้าไม่ซื้อ support อยู่แล้ว ผมคิดว่า Azul ยัง release เร็วกว่า Temurin (ขณะที่เขียนนี้ Azul ออก Java 17 หลายวันแล้ว ในขณะที่ Temurin ยังไม่เสร็จ)
  7. ผมยังไม่แน่ใจ JDK สายโม คือ Azul Prime (เสียเงิน) และ Alibaba Dragonwell ว่าควรเอามาใช้ดีไหม

ดังนั้นสำหรับงานบริษัทผมมี AWS support ก็ใช้ของ AWS ข้อจำกัดคือไม่มี Debian Docker ต้อง build เอง แต่ก็มี Dockerfile ตัวอย่างให้ (เค้าอยากเชียร์ให้ใช้ Amazon Linux)

Desktop

สำหรับบนคอมพิวเตอร์ส่วนตัว ผมคิดว่าสำหรับ developer บน server ใช้อะไรก็ใช้ให้ตรงกันน่าจะดีที่สุด สำหรับผมแล้วเป็น user มากกว่า ก็เลยเลือกใช้ดังนี้

  • Windows ใช้ Temurin จาก Chocolatey เพราะอยากได้ JRE อย่างเดียว (บางเจ้ามีให้โหลด แต่ Chocolatey ไม่มีแพคเกจ) และคิดว่ามันเป็นตัวที่แพร่หลายสุดน่าจะอัพเดตบ่อย
  • Linux ผมใช้ตัวที่อยู่ใน repository เลย คิดว่าถ้ามัน build มาโดยตรงน่าจะมีปัญหากับ OS น้อยที่สุด (ซึ่งบางทีก็อาจจะเป็นตัวเลือกที่ไม่ดี เช่น Debian เคย build จาก master ไปแล้วเจอบั๊ก)
  • Mac เดี๋ยวนี้ไม่ได้ใช้แล้วเลยไม่มีคำแนะนำ

Matrix

ลงเป็นตารางให้สุดท้าย ถ้าสงสัยแนะนำให้อ่านข้างบนก่อนครับ

หมายเหตุ:

  • ข้อมูลแพคเกจจาก Linux distro ต่างๆ ไม่ค่อยมี อาจจะลงไม่ถูกต้อง
  • ข้อมูลนี้ผมสำรวจก่อน OpenJDK 17 ออกเล็กน้อย
  • ข้อมูลนี้รวมเฉพาะ HotSpot runtime

ห้าม Compare Struct ใน Go

หลายเดือนก่อนทีมทำเรื่อง data encryption ใน Go ซึ่งเราจะมี struct EncryptedString ซึ่งเก็บ string แบบเข้ารหัสไว้แล้ว

ปัญหาก็คือเรากังวลว่า developer อาจจะคิดว่ามันใช้งานเหมือน string ปกติแล้วไป compare (encryptedA == encryptedB) ซึ่งมันอาจจะเท่าหรือไม่เท่ากันก็ได้ขึ้นอยู่กับ internal state ของ string ก็ต้องแจ้ง developer ไปว่าให้ใช้ Equal() เสมอ อย่าใช้ ==

บังเอิญช่วงนั้นผมเขียน Protobuf อยู่พอดีเลยไปเห็น google.golang.org/protobuf/internal/pragma ซึ่งผมว่ามันมีแต่ hack เทพๆ ที่อ่านแล้วฉลาดขึ้นเลย

DoNotCompare

Hyrum’s Law บอกว่าถ้ามีคนใช้ API มากพอ ต่อให้ไม่ได้บอกว่าใช้แบบนี้ได้ มันจะมีคนไปใช้ตามพฤติกรรมที่สังเกตได้เสมอ ซึ่งเราอาจจะเคยได้ยินว่าบางระบบอาจจะถึงกับต้อง emulate bug เพื่อให้มันใช้งานได้ เช่นใน StarCraft Remastered นั้น Blizzard ถึงกับต้องจำลอง buffer overflow เพราะ map เก่าๆ บางอันใช้

ในเคสของ Protobuf แปลว่าถ้าใช้ == กับ Protobuf message ได้แถม return ค่าถูกต้อง ก็คงจะมีคนใช้ == แน่นอน พอออก version ใหม่ที่เปลี่ยนโครงสร้างของ struct ก็พัง ทั้งๆ ที่ไม่เคยบอกว่าใช้ == ได้ ดังนั้น Go Protobuf เลยมี “pragma” หรือสูตรสำเร็จที่ฝังเข้าไปใน struct เพื่อทำให้พฤติกรรมต้องห้ามนั้นมี error หรือ warning เกิดขึ้นได้

ในบล็อคนี้ผมจะพาไล่ทั้ง 4 pragma ในแพคเกจขณะที่เขียนนี้ พร้อมทั้งเฉลยว่ามันทำได้อย่างไร อ่านแล้วผมแนะนำให้ลองคิดตามก่อนที่จะอ่านเฉลยครับ

โค้ด DoNotCompare มีอยู่ว่า

type DoNotCompare [0]func()

โดยเวลาใช้งานให้ฝังเข้าไปใน struct เช่น

type X struct {
    Value int

    DoNotCompare
}

เมื่อเรา compile จะได้ข้อความว่า

./prog.go:17:20: invalid operation: a == b (struct containing DoNotCompare cannot be compared)

เหตุผลที่เป็นอย่างนี้เพราะใน Go เราไม่สามารถเปรียบเทียบ function ได้

./prog.go:17:31: invalid operation: fmt.Println == fmt.Println (func can only be compared to nil)

การ compare struct นั้นมันจะ compare ทุก field ภายใน struct เมื่อเจอ field นี้ที่ type เป็น function ซึ่ง compare ไม่ได้แล้ว compiler จึงจะไม่ยอมให้ compile

ส่วน [0] ด้านหน้า ไว้เดี๋ยวจะเฉลยครับ…

DoNotCopy

ใน Go เราอาจจะมีการ pass by value ในบางครั้งซึ่งมันจะ copy object เข้าไปใน function เช่น

type X struct {
    Value string
}

func RecvX(v X) {
    v.Value = "changed"
}

func main() {
    x := X{value: "init"}
    RecvX(x)
    fmt.Println(x.Value)
}

ในกรณีนี้ เนื่องจาก RecvX ได้รับ X แบบ pass by value ทำให้ v เป็น copy ของ x ใน main เมื่อ print ออกมาจะได้ผลลัพท์ว่า “init” ไม่ใช่ “changed”

เพื่อป้องกันการใช้งานแบบ pass by value โค้ด DoNotCopy จึงมีดังนี้

type DoNotCopy [0]sync.Mutex

โค้ดนี้จะต่างกับ DoNotCompare คือสามารถ compile ผ่านได้ตามปกติ แต่ถ้ารัน go vet แล้วจะมี error ดังนี้

./prog.go:16:14: RecvX passes lock by value: play.X contains sync.Mutex

สำหรับคนที่เคยใช้งาน sync.Mutex แล้วน่าจะทราบดีว่าห้าม copy sync.Mutex เด็ดขาดเนื่องจากจะทำให้มี lock 2 ชุดแทนที่จะมีชุดเดียว ซึ่ง go vet จับให้อยู่แล้ว ดังนั้นการฝัง sync.Mutex เข้าไปใน struct ถึงแม้ไม่ได้ใช้ก็ทำให้ go vet ทำงาน

คำถามจากหัวเรื่องที่แล้วคือ แล้ว [0] คืออะไร? syntax นี้ผู้เริ่มต้น Go บางคนอาจจะเรียน บางคนอาจจะข้ามไปเลยเพราะไม่ค่อยได้ใช้ syntax นี้คือ fixed size array ใน Go เช่น [10]int คือ slice ที่มีสมาชิกเป็น int 10 ตัวไม่สามารถเพิ่มลดได้ (และมันเป็นคนละ type ไม่ใช่ slice ใช้แทนที่ slice ปกติไม่ได้)

เนื่องจากจำนวนสมาชิกเป็นค่าที่ทราบตั้งแต่ compile ทำให้ compiler สามารถจอง memory ให้สมาชิกทั้ง 10 ตัวได้ล่วงหน้า ในกรณีของ pragma 2 ตัวก่อนหน้านี้เราระบุเป็น [0] ก็คือไม่มีสมาชิก ทำให้ compiler ไม่จองพื้นที่สำหรับเก็บข้อมูลไว้เลย ทำให้ไม่มี overhead จากการใช้งาน

NoUnkeyedLiterals

ปัญหาถัดมาที่เจอคือมันจะมีคนมักง่าย แบบนี้…

type X struct {
    Value1 string
    Value2 int
}

func main() {
    X{"v", 1}
}

พอสร้าง struct โดยไม่ระบุชื่อ field ปัญหาที่ตามมาคือถ้าจะเพิ่ม field คนที่เพิ่มจะกรีดร้องเพราะต้องไล่ตามแก้ของเก่าให้หมด

โค้ด NoUnkeyedLiteral ง่ายๆ แค่

type NoUnkeyedLiterals struct{}

ใช่แล้ว มันคือ struct ง่ายๆ ไม่มีอะไรเลย เนื่องจากมันจะทำให้ struct มี 3 field (ไม่ใช้ 3 field) โค้ดด้านบนจึงจะเจอ

./prog.go:14:27: too few values in X{...}

ทำให้ไม่สามารถระบุแค่ 2 field ที่ใช้งานได้ ต้องระบุค่าของ NoUnkeyedLiteral ด้วย ก็คือต้องพิมพ์

X{"v", 1, NoUnkeyedLiteral{}}

ซึ่งก็ควรจะเอะใจได้แล้วนะว่าเค้าไม่ให้ทำแบบนี้ !!

DoNotImplement

Pragma ตัวสุดท้ายคือ DoNotImplement ซึ่งไม่อนุญาตให้แพคเกจอื่น implement interface นี้ เนื่องจากใน Go นั้น interface ไม่จำเป็นต้องประกาศ implement ขอแค่มี method ตรงกันเป็นอันใช้ได้ จึงไม่สามารถประกาศ interface เป็น private ได้

โค้ดของ DoNotImplement หน้าตาแบบนี้

type DoNotImplement interface{ ProtoInternal(DoNotImplement) }

โดยการใช้งานก็ให้ฝังเข้าไปใน Interface อื่น

type Printable interface {
    Print()

    DoNotImplement
}

ผมใช้เวลาหลายชั่วโมงพยายามทำความเข้าใจโค้ดนี้ ใคร declare ProtoInternal? แล้วมัน recursive มันช่วยยังไง?

ถ้าจัดรูปใหม่แล้ว มันจะเป็น

type DoNotImplement interface {
    ProtoInternal(value DoNotImplement)
}

ก็คือเป็น interface ที่มี 1 method ชื่อ ProtoInternal รับ argument 1 อย่างเป็น type ตัวเอง

แต่ เอ มันก็แก้ด้วยการเขียน implementation ง่ายๆ แบบนี้

func (x *X) ProtoInternal(value DoNotImplement) {}

หรือเปล่า….

ผมอ่าน docs อยู่ 2 ชั่วโมงก่อนจะไปสังเกตชื่อแพคเกจ google.golang.org/protobuf/internal/pragma

ใน Go แพคเกจจะไม่อนุญาตให้ import package ที่มี path internal มาจากแพคเกจอื่นๆ ดังนั้นถ้าจะ implement interface นี้ได้จริง ก็ต้อง import DoNotImplement มาใส่ใน argument ให้ได้ด้วย และเนื่องจากว่ามันเป็น internal จึงมีเฉพาะ package ที่ได้รับอนุญาตเท่านั้นจึงจะ import ได้

ทริคนี้ยังใช้ใน NoUnkeyedLiteral ในหัวข้อก่อนหน้าด้วย โดยถ้าจะดันทุรังใช้ X{"v", 1, NoUnkeyedLiteral{}} ก็จะเจอปัญหาเหมือนกันว่า import NoUnkeyedLiteral ไม่ได้

Genius

ทั้ง 4 ทริคนี้ผมว่าเป็นโค้ดที่เขียนได้ฉลาดมากๆ เหมือนเป็น puzzle และตอนอ่านผมก็คิดว่ามันลับสมองมากๆ ที่จะไล่ดูกลวิธีของผู้สร้างว่าเค้าวางกับดักโดยใช้แค่ Go standard tools ได้อย่างไร

ในทีมผมก็คุยกันแล้วว่าทริคลักษณะนี้อาจจะยังไม่อยากใช้เท่าไรนักเพราะภายในบริษัทอาจจะยังพอตกลงกันหรือตรวจกันใน code review ได้โดยไม่ต้องพึ่ง hack แต่ในระดับ protobuf ที่เป็น library ใช้กันแพร่หลายแล้วการบังคับทางเทคนิคนี้ก็เป็นเรื่องจำเป็นที่จะไม่ให้คนเขียนโค้ดแบบ genius เล่นท่าพิสดารที่อัพเกรดไม่ได้