23 điểm bởi GN⁺ 2025-12-31 | 1 bình luận | Chia sẻ qua WhatsApp
  • Giới thiệu một mẹo giúp có thể chạy trực tiếp file Go như file thực thi
  • Chỉ cần đặt dòng đầu tiên là //usr/local/go/bin/go run "$0" "$@"; exit và cấp quyền thực thi, rồi có thể chạy bằng ./script.go
  • Cách này không phải shebang mà tận dụng cơ chế shell fallback sang /bin/sh khi POSIX gặp ENOEXEC
  • Shell sẽ thực thi dòng đầu tiên như một lệnh, còn trình biên dịch Go sẽ nhận diện nó là chú thích // và bỏ qua
  • "$0" truyền vào đường dẫn của chính nó để go run build và chạy script, còn $@ dùng để truyền tham số
  • Thư viện chuẩn mạnh mẽ và cam kết tương thích ngược của Go rất phù hợp cho mục đích scripting; chỉ cần dùng Go 1.x thì script có thể chạy ổn trong nhiều thập kỷ
  • Có thể tránh được sự phức tạp trong quản lý phụ thuộc như virtual environment của Python, pip/poetry/uv, v.v.

Cách hoạt động của shebang giả

  • shebang (#!) là cách chỉ định trình thông dịch thông qua system call execve, nhưng kỹ thuật được giới thiệu trong bài này không phải shebang
  • Dòng đầu tiên của file mã nguồn Go sẽ là //usr/local/go/bin/go run "$0" "$@"; exit, phía dưới package main là mã Go thông thường
    • Cấp quyền thực thi bằng chmod +x script.go là có thể chạy như ./script.go
  • Khi kiểm tra bằng strace, có thể thấy lúc shell thử chạy ./script.go bằng execve thì kernel trả về ENOEXEC (Exec format error)
    • Shell nhận ENOEXEC sẽ fallback sang /bin/sh để diễn giải file đó như shell script
    • Trong shell, // không phải là chú thích mà được diễn giải là đường dẫn gốc (/), nên //usr/local/go/bin/go vẫn là một đường dẫn hợp lệ để thực thi
  • Vì vậy dòng đầu tiên //usr/local/go/bin/go run "$0" "$@"; exit sẽ được shell thực thi như một lệnh
    • "$0" truyền vào đường dẫn file đang được chạy, nên trong lệnh thực thi, "$0" sẽ trở thành đường dẫn script.go, để go run tự tìm chính nó rồi build và chạy
    • "$@" là phép mở rộng positional parameters từ đối số thứ nhất trở đi, cho phép các lệnh gọi như ./script.go -f flag0 here are some args
    • Nếu không có ; exit thì sh sẽ tiếp tục diễn giải file Go theo từng dòng và sẽ lỗi ở các token như package

Vì sao Go phù hợp để scripting

  • Cam kết tương thích ngược của Go là năng lực cốt lõi, nên chỉ cần dùng Go 1.x thì script đã viết có thể hoạt động lâu dài
  • Thư viện chuẩn phát triển tốt cùng các công cụ tích hợp sẵn (formatter, linter, v.v.) được cung cấp mà không cần cấu hình riêng, giúp tối đa hóa khả năng chia sẻ script và tính di động
    • Có thể chạy code mà không cần học virtual environment hay nhiều package manager khác nhau như pip, poetry, uv như với Python
    • Nhờ các công cụ tích hợp sẵn trong hệ sinh thái Go và khả năng tích hợp IDE, có thể dùng formatter và linter mặc định mà không cần .pyproject hay package.json
  • Chỉ cần cài Go phiên bản mới là có thể chạy trên bất kỳ OS nào trong nhiều thập kỷ tới

So sánh với các ngôn ngữ biên dịch khác

  • Rust có tốc độ biên dịch chậm, thư viện chuẩn yếu hơn nên gần như bắt buộc phải dùng dependency, đồng thời đòi hỏi tính hoàn hảo nên tốc độ phát triển chậm hơn
  • Java và các ngôn ngữ JVM vốn đã có các ngôn ngữ scripting dựa trên JVM bytecode, và Kotlin scripting gọn nhẹ cũng có thể là một lựa chọn thay thế
  • Trong số các ngôn ngữ biên dịch, Go sở hữu những đặc tính phù hợp nhất cho mục đích scripting

Vấn đề định dạng của gopls và cách khắc phục

  • gopls yêu cầu có khoảng trắng sau chú thích (//example// example), nên dòng shebang giả sẽ bị phá vỡ
  • Nếu có khoảng trắng thì sẽ thành // usr/local/go/bin/go, và shell sẽ không còn nhận đó là đường dẫn nữa
  • Cách khắc phục: dùng đề xuất từ thread HN, là dùng chú thích khối /**/ thay cho //
    • Viết theo dạng /*usr/local/go/bin/go run "$0" "$@"; exit; */
    • Bắt buộc phải có dấu chấm phẩy (;) sau exit

1 bình luận

 
GN⁺ 2025-12-31
Ý kiến trên Hacker News
  • Phần tác giả nói “không muốn bận tâm tới pip vs poetry vs uv” thì thật ra uv hỗ trợ trực tiếp chính trường hợp sử dụng này
    Chỉ cần cài Python phiên bản phù hợp và uv, kể cả phụ thuộc từ PyPI cũng được xử lý
    Liên kết tài liệu chính thức của uv

    • Thậm chí còn có cách tốt hơn nữa
      #!/usr/bin/env -S uv run --python 3.14 --script
      Làm vậy thì ngay cả khi Python chưa được cài sẵn, uv cũng sẽ tải đúng phiên bản được chỉ định rồi chạy
    • Tôi cũng từng nghĩ vậy, nhưng với người không dùng Python thì chuyện này vẫn chưa thực sự trực quan
      Khi mới tiếp xúc với Clojure, đa số sẽ được khuyên dùng Leiningen, còn với Python thì tìm kiếm ra đủ thứ như venv, poetry, hatch, uv
      uv đang dần trở thành xu hướng chính, nhưng vẫn chưa thật sự phổ biến
      Trước đây tôi từng cài Go bằng apt rồi phải cài lại vì phiên bản quá cũ, nhưng chuyện đó được giải quyết nhanh hơn nhiều
      Vấn đề môi trường ảo của Python vẫn còn phức tạp
    • Tôi đã giải quyết chuyện này từ năm 2019 bằng PyFlow
      Đây là một công cụ OSS viết bằng Rust, tự động quản lý phiên bản Python và venv
      Chỉ cần cấu hình pyproject.toml rồi chạy pyflow main.py, nó sẽ cài và khóa phụ thuộc như Cargo, đồng thời tự khớp phiên bản Python phù hợp với dự án
      Hồi đó Poetry và Pipenv đang phổ biến, nhưng vẫn chưa đủ tốt ở phần venv và quản lý phiên bản
    • Tôi cũng đã chuyển gần như hoàn toàn sang uv
      Chủ yếu dùng uv add, chỉ khi cần mới dùng uv pip
      Nhưng uv pip vẫn giữ nguyên giới hạn của pip — cách phân giải phụ thuộc thay đổi tùy theo thứ tự cài đặt
      uv pip install dep-a rồi cài dep-b, đổi thứ tự hai lệnh đó, hoặc cài cả hai cùng lúc đều cho ra kết quả khác nhau
      Đây gần như là vấn đề của pip, nhưng sự hỗn loạn trong quản lý gói Python vẫn còn nguyên
    • Thật ra thậm chí cũng không cần chỉ định phiên bản Python
      uv sẽ tự tải về
  • Go đã chủ động từ chối hỗ trợ shebang
    Thay vào đó, cách được khuyến nghị là dùng gorun
    Có thể chạy bằng mẹo POSIX như /// 2>/dev/null ; gorun "$0" "$@" ; exit $?
    Nim, Zig, D cũng có thể dùng tương tự với tùy chọn -run, còn Swift, OCaml, Haskell thì có thể chạy trực tiếp file
    Liên kết thảo luận liên quan

    • Với các script nhỏ, trình thông dịch yaegi có thể phù hợp hơn go run
      yaegi GitHub
  • Bài kiểu “tôi không muốn biết pip, poetry, uv khác nhau ra sao, tôi chỉ muốn chạy code” rốt cuộc là vấn đề về mức độ thành thạo kỹ thuật
    uv runPEP 723 đã giải quyết xong mọi chuyện

    • Đúng, nhưng uv run xuất hiện quá muộn
      Tôi dùng Python hơn 20 năm rồi mà vẫn luôn ngại các codebase có package bên ngoài hoặc venv
      Nhờ uv run nên toàn bộ dự án công ty của tôi đã chuyển sang đó, nhưng dự án cá nhân thì tôi đã sang Go rồi
      Về lâu dài tôi vẫn thích ngôn ngữ kiểu tĩnh hơn
    • Với một ngôn ngữ lâu đời thì cuối cùng bạn cũng sẽ phải học các thư viện cạnh tranh nhau
    • Đây là vấn đề UX
      Người dùng chỉ muốn chương trình chạy được
      uv run và PEP 723 đã giải quyết bài toán, nhưng việc vẫn phải biết đến uv khiến rào cản gia nhập còn tồn tại
      Chừng nào uv chưa trở thành công cụ mặc định chính thức, sẽ còn nhiều người rời bỏ Python
  • Tôi nghĩ đây là một ý tưởng thật sự thiên tài
    Nhưng scripting cần một cảm giác công thái học khác với phần mềm để triển khai/phân phối
    bash mang tính ngẫu hứng, Go hợp để sản phẩm hóa, Python nằm đâu đó ở giữa, Ruby gần với bash hơn, còn Rust nghiêng về phía Go
    Script hữu ích khi cần ghép nhanh các lệnh hệ điều hành để xử lý tác vụ một lần
    Go thiếu đi sự ngẫu hứng đó

    • Tôi cũng đồng ý với nhận định Python “ở giữa”
      Tôi từng thử chạy một ứng dụng gtk đơn giản trên Debian bằng uv, phụ thuộc có vẻ đều đúng nhưng vẫn không chạy được và cuối cùng Core Dump
      Mỗi lần tôi thử lại Python theo hướng mới thì gần như đều gặp kiểu chuyện này
      Go thì dài dòng hơn, nhưng một khi đã biên dịch xong thì nó cứ thế mà chạy
    • Tôi cũng cảm thấy tương tự
      Điểm mấu chốt là liệu có thể kết thúc trong một file duy nhất hay không
      Script Go dài 500 dòng vẫn làm được, nhưng bản thân ngôn ngữ này giả định nhiều file và module
      Việc không hỗ trợ bang-line cũng vì lý do đó
      Đằng nào go run cũng tạo binary tạm thời, nên theo tôi thà build luôn rồi đặt vào /usr/local/bin còn hơn
    • Nói rằng bash gần với lệnh hệ điều hành hơn là một hiểu lầm
      bash cũng chỉ là một lớp trừu tượng phía trên hệ điều hành giống như Python, chỉ khác là nó là shell mặc định nên tạo cảm giác như vậy
    • Trong thời đại LLM có thể sửa code thay con người, có thể tính dễ đọc sẽ trở nên quan trọng hơn công thái học khi viết
      Đặc biệt theo hướng làm cho code do LLM viết ra dễ đọc hơn với con người
  • Tôi đồng ý rằng người mới dùng Python không cần biết sự khác nhau giữa pip, poetry và uv
    Nhưng nếu đã viết blog về chủ đề này thì ít nhất phải biết uv giải quyết được vấn đề
    Phê phán trong trạng thái thiếu hiểu biết thì không thuyết phục

    • Có người hỏi liệu uv có giải quyết được kiểu “write once, run anywhere” như Go hay không
      Tôi cũng tò mò vì bản thân chưa hiểu hoàn toàn khái niệm của uv
  • Tôi thích viết script bằng Python
    Làm việc nhanh, xử lý những thứ đơn giản mà không phải lo quá nhiều về kiểu hay bộ nhớ
    Nhưng tôi không muốn dùng nó cho ứng dụng quy mô lớn

    • Tôi cũng thích Python để scripting, nhưng lại ghét cài script của người khác
    • Đây là một cách tiếp cận thiên về Linux
      Hầu hết hệ thống đều có sẵn Python nên với script đơn giản là đủ dùng
      Nghĩ đến chuyện phải cài Go thì tôi thấy dùng Python qua uv còn hợp lý hơn
      Như chính tác giả nói là “ban đầu hơi mang tính troll”, nên cuối cùng đây vẫn là chuyện thích Go hơn thôi
    • Tôi thấy JS cũng không tệ như một ngôn ngữ scripting
      Chỉ cần node bla.js là xong
    • Kiểu dữ liệu thì lúc nào cũng phải để tâm
      Bạn cần biết hàm trả về gì, và khi đã quen ngôn ngữ thì các kiểu cơ bản có thể nhớ bằng trực giác
      Ngôn ngữ kiểu tĩnh cũng vậy thôi
    • Python rất tuyệt cho lập trình viên, nhưng lại là ác mộng khi phân phối hoặc tích hợp
      Nếu có nghĩ đến người khác thì đừng viết mã để phân phối bằng Python
  • Tôi tưởng sẽ là một bài chê Python, ai ngờ lại là một mẹo hữu ích
    Nếu là ngôn ngữ dùng // làm chú thích thì có thể áp dụng biến thể của mẹo này
    C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL đều làm được
    Đặc biệt ý tưởng làm demo đồ họa một file bằng GLSL khá thú vị
    Ví dụ Shadertoy
    Với C còn có thể dùng chú thích khối theo kiểu /*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */

    • Swift có dự án swift-sh cho phép chạy script có phụ thuộc bên ngoài
      Nó giống uv nhưng dành cho Swift
      Swift cũng chính thức hỗ trợ shebang
    • Trong C/C++ thì có thể viết trực tiếp #!
      Thời TCC trước đây tôi từng dùng kiểu này để “script bằng C”
      Với dự án lớn thì build script sẽ đọc manifest, build xong rồi chạy
      Nhưng vì khó kiểm soát môi trường nên không phù hợp cho công việc thực tế
    • Rust không cần mấy mẹo kiểu này
      Nó hỗ trợ shebang trực tiếp
  • Nếu muốn một ngôn ngữ công thái học hơn thì .NET 10 cũng có tính năng “run file directly”
    Nó hỗ trợ shebang và tự động cài package ngay trong script
    Có thể dùng chỉ thị #:sdk để chạy thẳng cả web app

    • Hôm nay tôi cũng vừa viết thử script C# bằng tính năng này lần đầu, trải nghiệm khá ổn
      Chỉ là phần biên dịch AOT vẫn còn hơi thô
  • Ban đầu tôi nghĩ đây sẽ là bài chê Python, nhưng ngược lại nó khiến tôi nghĩ nhiều hơn về hướng đi của hệ sinh thái ngôn ngữ
    Việc ML bị trói vào Python theo tôi là một sai lầm lớn
    Vì nó chậm, hệ thống kiểu bất tiện và khó phân phối
    Giờ nên cân nhắc các lựa chọn khác như TypeScript, Go, Rust

    • Đồng ý
      Nhưng lý do ML chọn Python là vì FFI dựa trên C
      NodeJS, Rust, Go đều yếu hơn ở FFI
      Python có lợi thế lớn ở điểm này
      Lý tưởng nhất là một ngôn ngữ đơn giản như Python nhưng có hệ thống kiểu tốt hơn và cơ chế phân phối tốt hơn
    • Tôi không đồng ý chuyện thay bằng TypeScript
      Tôi không muốn lấy một ngôn ngữ từ hệ sinh thái JS để thay Python
    • ML đi theo Python là do áp lực thị trường
      Lisp hay Lua (Torch) vốn phù hợp hơn, nhưng Python được chọn vì sự đơn giản
      Tôi cũng đang phát triển một framework ML dựa trên Lisp, nhưng có lẽ rất khó được chấp nhận
    • Địa ngục phụ thuộc của Python vẫn rất nghiêm trọng
      Vấn đề tương thích phiên bản, thiếu semver, hệ sinh thái thiếu ổn định khiến nó còn tụt lại hơn cả JS
      JS/Node đã trưởng thành trong 10 năm qua, còn Python dường như vẫn đứng ở năm 2012
      Thật đáng tiếc khi ML lại tiêu chuẩn hóa trên Python
    • Tôi muốn một ngôn ngữ đơn giản, giàu khả năng biểu đạt nhưng vẫn kiểu mạnh + biên dịch native
      Khi làm công cụ CLI, Go nhanh hơn Python rất nhiều
      Tôi đã quay lại Python vì chênh lệch LOC, nhưng mỗi lần chạy lại thấy nhớ Go
      Có lẽ OCaml mới là lý tưởng, chỉ tiếc bộ công cụ quá cũ kỹ
  • Vấn đề của script Go là dòng đầu tiên không được có khoảng trắng
    gopls ép tự động định dạng
    Trong CI cũng cần giữ định dạng nhất quán, nên điều này có ý nghĩa thực tế
    Nhưng vấn đề lớn hơn là không thể dùng go.mod
    Tức là không thể ghim phiên bản phụ thuộc, nên khó đảm bảo tương thích

    • Dù vậy, phiên bản major vẫn được khóa qua import path nên mặc định vẫn có mức tương thích nhất định
    • Đây là vấn đề tương thích ở cấp ngôn ngữ/runtime, chứ không hẳn là vấn đề phụ thuộc