- แนะนำเทคนิคที่ทำให้ไฟล์ Go รันได้โดยตรงเหมือนไฟล์ executable
- ถ้าใส่
//usr/local/go/bin/go run "$0" "$@"; exit ไว้บรรทัดแรกและให้สิทธิ์รัน ก็จะเรียกใช้ด้วย ./script.go ได้
- วิธีนี้ไม่ใช่ shebang แต่ใช้ พฤติกรรม fallback ของเชลล์ไปที่ /bin/sh เมื่อเกิด ENOEXEC บน POSIX
- เชลล์จะรันบรรทัดแรกเป็นคำสั่ง ขณะที่คอมไพเลอร์ Go มองเป็นคอมเมนต์
// แล้วข้ามไป
- ส่งพาธของตัวเองผ่าน
"$0" ทำให้ go run สร้างและรันสคริปต์นั้นเอง พร้อมส่งอาร์กิวเมนต์ต่อผ่าน $@
- standard library ที่แข็งแกร่งและการรับประกัน backward compatibility ของ Go ทำให้เหมาะกับงานสคริปต์ และตราบใดที่ยังใช้ Go 1.x สคริปต์ก็อาจทำงานได้ต่อเนื่องเป็นสิบ ๆ ปี
- สามารถหลีกเลี่ยง ความซับซ้อนของการจัดการ dependency เช่น virtual environment, pip/poetry/uv ของ Python ได้
หลักการทำงานของ fake shebang
- shebang(
#!) คือวิธีกำหนด interpreter ผ่าน system call execve แต่เทคนิคในบทความนี้ไม่ใช่ shebang
- รูปแบบคือใส่
//usr/local/go/bin/go run "$0" "$@"; exit ไว้บรรทัดแรกของไฟล์ซอร์ส Go แล้วตามด้วยโค้ด Go ปกติภายใต้ package main
- ให้สิทธิ์รันด้วย
chmod +x script.go แล้วจะเรียกใช้แบบ ./script.go ได้
- เมื่อตรวจด้วย
strace จะเห็นว่าเมื่อเชลล์พยายามรัน ./script.go ผ่าน execve เคอร์เนลจะคืนค่า ENOEXEC (Exec format error)
- เมื่อได้รับ ENOEXEC เชลล์จะ fallback ไปใช้
/bin/sh เพื่อตีความไฟล์นั้นเป็นเชลล์สคริปต์
- ในเชลล์
// ไม่ใช่คอมเมนต์ แต่ถูกตีความเป็น พาธราก (/) ดังนั้น //usr/local/go/bin/go จึงยังเป็นพาธที่ใช้งานได้ตามปกติ
- ดังนั้นบรรทัดแรก
//usr/local/go/bin/go run "$0" "$@"; exit จึงถูกเชลล์รันเป็นคำสั่ง
"$0" จะส่งพาธของไฟล์ที่ถูกเรียกใช้งาน ดังนั้นในคำสั่งรัน "$0" จะกลายเป็นพาธของ script.go ทำให้ go run หาไฟล์ตัวเองแล้ว build และรันต่อได้
"$@" คือการขยาย positional arguments ตั้งแต่อาร์กิวเมนต์ตัวที่ 1 เป็นต้นไป จึงรองรับการเรียกแบบ ./script.go -f flag0 here are some args
- ถ้าไม่มี
; exit แล้ว sh จะพยายามตีความไฟล์ Go ต่อทีละบรรทัด และจะเกิด error ที่โทเคนอย่าง package
ทำไม Go ถึงเหมาะกับงานสคริปต์
- จุดสำคัญคือ การรับประกัน backward compatibility ของ Go ทำให้ตราบใดที่ยังใช้ Go 1.x สคริปต์ที่เขียนไว้ก็ยังทำงานได้ในระยะยาว
- Go มี standard library ที่พัฒนาอย่างดี และเครื่องมือในตัว (formatter, linter ฯลฯ) ให้ใช้งานโดยไม่ต้องตั้งค่าเพิ่ม จึงช่วย เพิ่มความสามารถในการแชร์สคริปต์และการพกพาข้ามระบบได้สูงสุด
- ต่างจาก Python ที่มักต้องเรียนรู้ virtual environment หรือ package manager หลายแบบ (pip, poetry, uv) ก่อนจึงจะรันโค้ดได้
- ด้วยเครื่องมือใน ecosystem ของ Go และการเชื่อมกับ IDE จึงสามารถใช้ formatter และ linter ได้เป็นค่าเริ่มต้นแม้ไม่มี
.pyproject หรือ package.json
- ขอแค่ติดตั้ง Go รุ่นใหม่ไว้ ก็สามารถ รันได้บนทุก OS ไปอีกหลายสิบปี
เปรียบเทียบกับภาษาแบบคอมไพล์อื่น
- Rust คอมไพล์ช้า standard library ยังไม่แข็งแรงพอจนมักต้องพึ่ง dependency และมีแนวโน้มเน้นความสมบูรณ์แบบจนพัฒนาได้ช้ากว่า
- Java และภาษาในตระกูล JVM มีภาษาสคริปต์บน JVM อยู่แล้ว และ Kotlin scripting แบบ lightweight ก็อาจเป็นอีกทางเลือก
- ในบรรดาภาษาแบบคอมไพล์ Go มี คุณลักษณะที่เหมาะกับการใช้ทำสคริปต์มากที่สุด
ปัญหาการฟอร์แมตของ gopls และวิธีแก้
gopls จะบังคับให้มีช่องว่างหลังคอมเมนต์ (//example → // example) จึงทำให้ บรรทัด fake shebang พัง
- ถ้ามีช่องว่างจะกลายเป็น
// usr/local/go/bin/go ซึ่งเชลล์จะไม่มองเป็นพาธ
- วิธีแก้: ใช้ข้อเสนอจาก เธรด HN โดยเปลี่ยนจาก
// เป็นบล็อกคอมเมนต์ /**/
- เขียนเป็น
/*usr/local/go/bin/go run "$0" "$@"; exit; */
- ต้องมี semicolon (
;) หลัง exit เสมอ
1 ความคิดเห็น
ความเห็นจาก Hacker News
ส่วนที่ผู้เขียนบอกว่า “ไม่อยากมานั่งสนใจ
pipvspoetryvsuv” จริงๆ แล้ว uv รองรับกรณีใช้งานนี้โดยตรงรวมถึง dependency จาก PyPI ด้วย ขอแค่ติดตั้ง Python เวอร์ชันที่ต้องการและ uv ก็พอ
ลิงก์เอกสารทางการของ uv
#!/usr/bin/env -S uv run --python 3.14 --scriptแบบนี้ถึงจะไม่ได้ติดตั้ง Python เอาไว้ uv ก็จะดาวน์โหลดเวอร์ชันที่ระบุมาแล้วรันให้
ถ้าเริ่มจับ Clojure ครั้งแรก คนส่วนใหญ่มักจะได้คำแนะนำให้ใช้ Leiningen แต่ถ้าค้นเรื่อง Python จะเจอทั้ง venv, poetry, hatch, uv และอีกหลายอย่าง
uv กำลังค่อยๆ กลายเป็นตัวเลือกหลักก็จริง แต่ตอนนี้ยังไม่ถือว่าแพร่หลายทั่วไป
ผมเคยติดตั้ง Go ผ่าน apt แล้วเจอว่าเวอร์ชันเก่าเกินจนต้องลงใหม่ ซึ่งจัดการได้เร็วกว่ามาก
ปัญหาเรื่อง virtual environment ของ Python ยังซับซ้อนอยู่
มันเป็นเครื่องมือ OSS ที่เขียนด้วย Rust สำหรับจัดการ Python version และ venv อัตโนมัติ
แค่ตั้งค่า
pyproject.tomlแล้วรันpyflow main.pyมันก็จะติดตั้งและล็อก dependency แบบเดียวกับ Cargo พร้อมทั้งจัด Python เวอร์ชันให้ตรงกับโปรเจ็กต์อัตโนมัติตอนนั้น Poetry กับ Pipenv กำลังนิยม แต่ยังจัดการทั้ง venv และ version ได้ไม่ครบ
ส่วนมากใช้
uv addและจะใช้uv pipเฉพาะเวลาจำเป็นแต่
uv pipก็ยังติดข้อจำกัดแบบเดียวกับ pip — การแก้ dependency เปลี่ยนไปตามลำดับการติดตั้งการรัน
uv pip install dep-aแล้วค่อยลงdep-bจะได้ผลไม่เหมือนกับสลับลำดับ หรือไม่เหมือนกับลงพร้อมกันทีเดียวอันนี้ใกล้เคียงจะเป็นปัญหาของ pip มากกว่า แต่ความ สับสน ในการจัดการแพ็กเกจของ Python ก็ยังคงอยู่
uv จะดาวน์โหลดให้เอง
Go ปฏิเสธการรองรับ shebang อย่างชัดเจน
แนะนำให้ใช้
gorunแทนสามารถใช้ลูกเล่นแบบ POSIX อย่าง
/// 2>/dev/null ; gorun "$0" "$@" ; exit $?เพื่อรันได้Nim, Zig, D ก็ใช้แบบคล้ายกันได้ผ่านออปชัน
-runส่วน Swift, OCaml, Haskell สามารถรันไฟล์ได้โดยตรงลิงก์ประเด็นถกเถียงที่เกี่ยวข้อง
go runyaegi GitHub
ข้อความแนว “ไม่อยากรู้ความต่างของ pip, poetry, uv แค่อยากรันโค้ด” สุดท้ายก็คือเรื่องของ ระดับความชำนาญทางเทคนิค
uv runกับ PEP 723 แก้ปัญหาทั้งหมดนี้ได้แล้วuv runจะมา มันใช้เวลานานเกินไปผมใช้ Python มาเกิน 20 ปีแล้ว แต่ทุกครั้งที่เจอโค้ดเบสที่มี external package หรือ venv ก็ยังรู้สึกกลัวอยู่เสมอ
ตอนนี้โปรเจ็กต์ในบริษัทผมย้ายมาใช้
uv runหมดแล้ว แต่โปรเจ็กต์ส่วนตัวผมย้ายไป Go เรียบร้อยในระยะยาวผมชอบ ภาษาที่มี static type มากกว่า
ผู้ใช้แค่อยากให้โปรแกรมรันได้
uv runกับ PEP 723 แก้ปัญหาไปแล้วก็จริง แต่การที่ยังต้องไปรู้จัก uv ก่อนก็ยังเป็นกำแพงในการเริ่มต้นตราบใดที่ uv ยังไม่ใช่เครื่องมือพื้นฐานอย่างเป็นทางการ ผู้ใช้จำนวนมากก็จะยังหนีจาก Python ต่อไป
ผมคิดว่านี่เป็นไอเดียที่ อัจฉริยะมาก
แต่สคริปต์ต้องการความ ergonomic ที่ต่างจากซอฟต์แวร์สำหรับแจกจ่ายใช้งาน
bash เหมาะกับงานเฉพาะหน้า, Go เหมาะกับการทำเป็นผลิตภัณฑ์, Python อยู่ตรงกลาง, Ruby ใกล้กับ bash, ส่วน Rust ออกไปทาง Go
สคริปต์มีประโยชน์เวลาต้องเอาคำสั่ง OS มาต่อกันเร็วๆ เพื่อจัดการงานใช้ครั้งเดียว
Go ยังขาดความคล่องตัวแบบนั้น
ผมพยายามรันแอป gtk ง่ายๆ บน Debian ด้วย uv ทั้งที่ dependency ครบหมดแล้ว แต่สุดท้ายก็ยังรันไม่ได้และจบที่ Core Dump
ทุกครั้งที่ลองกลับมาใช้ Python ใหม่ก็มักเจออะไรแบบนี้
Go ถึงจะเขียนยืดยาวกว่า แต่พอคอมไพล์แล้วมันก็ทำงานเลย
ประเด็นสำคัญคือมันจบได้ในไฟล์เดียวหรือเปล่า
สคริปต์ Go ยาว 500 บรรทัดก็ทำได้ แต่ตัวภาษาถูกออกแบบบนสมมติฐานว่ามีหลายไฟล์และหลายโมดูล
การที่ bang-line ใช้ไม่ได้ก็มาจากเหตุผลเดียวกัน
ในเมื่อ
go runก็สร้างไบนารีชั่วคราวอยู่แล้ว ผมว่าคอมไพล์เลยแล้วเอาไปไว้ที่/usr/local/binยังดีกว่าbash ก็เป็นเพียงชั้นนามธรรมบน OS พอๆ กับ Python เพียงแต่คนรู้สึกแบบนั้นเพราะมันเป็นเชลล์มาตรฐาน
โดยเฉพาะการทำให้โค้ดที่ LLM เขียนมาอ่านง่ายสำหรับมนุษย์
ผมเห็นด้วยว่าผู้ใช้ที่เพิ่งเริ่มกับ Python ไม่จำเป็นต้องรู้ความต่างระหว่าง pip, poetry, uv
แต่ถ้าเป็นบล็อกเกอร์ที่เขียนเรื่องแนวนี้ อย่างน้อยก็ควรรู้ว่า uv แก้ปัญหานี้ได้
การวิจารณ์โดยไม่รู้ข้อมูลมันไม่น่าเชื่อถือ
ผมเองก็ยังไม่เข้าใจแนวคิดของ uv แบบครบถ้วน เลยอยากรู้เหมือนกัน
ผม ชอบ เขียนสคริปต์ด้วย Python
มันทำงานได้เร็วและเหมาะกับการจัดการงานง่ายๆ โดยไม่ต้องกังวลเรื่อง type หรือหน่วยความจำ
แต่ผมไม่อยากใช้มันกับแอปพลิเคชันขนาดใหญ่
ระบบส่วนใหญ่มี Python มาให้โดยปริยายอยู่แล้ว ซึ่งเพียงพอสำหรับสคริปต์ง่ายๆ
ถ้าคิดว่าต้องติดตั้ง Go เพิ่ม ผมกลับมองว่าใช้ Python ผ่าน uv ยังดีกว่า
อย่างที่ผู้เขียนบอกเองว่า “เริ่มต้นแบบกึ่งๆ ปั่น” สุดท้ายมันก็เป็นเรื่องของ ความชอบ Go
แค่
node bla.jsก็จบต้องรู้ว่าฟังก์ชันคืนค่าอะไร และถ้ารู้ภาษาดีพอ type พื้นฐานก็มักจำได้อยู่แล้ว
ภาษาที่มี static type ก็ไม่ต่างกัน
ถ้าคิดถึงคนอื่นด้วย ก็ไม่ควรเขียนโค้ดสำหรับแจกจ่ายด้วย Python
เดิมทีผมคาดว่าจะเป็นการด่า Python แต่กลับกลายเป็น ทิปที่มีประโยชน์
ถ้าเป็นภาษาที่ใช้
//เป็นคอมเมนต์ ก็สามารถประยุกต์ลูกเล่นนี้ได้ใช้ได้กับ C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL ฯลฯ
โดยเฉพาะการทำกราฟิกเดโมไฟล์เดียวด้วย GLSL นี่น่าสนใจมาก
ตัวอย่าง Shadertoy
ในภาษา C ยังใช้ block comment ทำแบบ
/*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */ได้ด้วยเป็นแนวคิดคล้าย uv สำหรับ Swift
และ Swift ก็รองรับ shebang อย่างเป็นทางการด้วย
#!ตรงๆ เลยก็ได้สมัย TCC ผมเคยใช้วิธีนี้ทำ “C scripting”
สำหรับโปรเจ็กต์ใหญ่จะมี build script อ่าน manifest แล้ว build จากนั้นค่อยรัน
แต่เพราะควบคุมสภาพแวดล้อมได้ยาก จึงไม่เหมาะกับงานจริง
เพราะรองรับ shebang โดยตรง
ถ้าต้องการภาษา ที่ ใช้งานสบายขึ้น .NET 10 ก็มีฟีเจอร์ “run file directly”
รองรับ shebang และติดตั้งแพ็กเกจจากในสคริปต์ให้อัตโนมัติ
ใช้ directive
#:sdkเพื่อรันเว็บแอปได้ทันทีด้วยแต่ AOT compile ยังดูหยาบๆ อยู่
ตอนแรกนึกว่าจะเป็นการโจมตี Python แต่กลับทำให้คิดถึง ทิศทางของ ecosystem ภาษา มากกว่า
ผมมองว่าการที่ ML ไปผูกกับ Python เป็นความผิดพลาดครั้งใหญ่
เพราะมันช้า ระบบ type ก็ไม่สะดวก และการแจกจ่ายก็ยาก
ตอนนี้ควรเริ่มพิจารณาทางเลือกอย่าง TypeScript, Go, Rust ได้แล้ว
แต่เหตุผลที่ ML เลือก Python ก็เพราะ FFI ที่อิง C
NodeJS, Rust, Go อ่อนกว่าในเรื่อง FFI
Python มีจุดแข็งตรงนี้
สิ่งที่น่าจะเป็นอุดมคติคือภาษาที่ง่ายแบบ Python แต่มีระบบ type และระบบแจกจ่ายที่ดีกว่า
ผมไม่อยากใช้ภาษาจาก ecosystem ของ JS มาแทน Python
Lisp หรือ Lua (Torch) น่าจะเหมาะกว่า แต่ Python ถูกเลือกเพราะความเรียบง่าย
ผมเองก็กำลังพัฒนา ML framework ที่อิง Lisp อยู่ แต่คงยากจะได้การยอมรับ
ทั้งปัญหาความเข้ากันได้ของเวอร์ชัน, การไม่มี semver, ecosystem ที่ไม่เสถียร ทำให้รู้สึกตามหลัง JS เสียอีก
JS/Node โตเป็นผู้ใหญ่มาแล้วในช่วง 10 ปีที่ผ่านมา แต่ Python ยังเหมือนติดอยู่ในปี 2012
น่าเสียดายจริงๆ ที่ ML มามาตรฐานบน Python
เวลาสร้างเครื่องมือ CLI Go เร็วกว่า Python มาก
ผมกลับมาใช้ Python เพราะจำนวน LOC ที่ต่างกัน แต่ทุกครั้งที่รันก็ยังคิดถึง Go
จริงๆ แล้ว OCaml น่าจะใกล้อุดมคติที่สุด แต่ tooling ที่ดูเก่าเป็นภาระอยู่
ปัญหาของ Go script คือบรรทัดแรกต้องไม่มีช่องว่างนำหน้า
เพราะ
goplsบังคับ auto formattingและใน CI ก็ต้องรักษาความสม่ำเสมอของ format ด้วย เรื่องนี้จึงสำคัญในทางปฏิบัติ
แต่ปัญหาที่ใหญ่กว่าคือ ใช้
go.modไม่ได้นั่นหมายความว่าไม่สามารถระบุเวอร์ชันของ dependency ได้ จึงรับประกันความเข้ากันได้ได้น้อยลง