21P by GN⁺ 8일전 | ★ favorite | 댓글 1개
  • Go 파일을 실행 파일처럼 직접 실행할 수 있는 트릭 소개
  • 첫 줄에 //usr/local/go/bin/go run "$0" "$@"; exit를 두고 실행 권한을 주면 ./script.go로 실행 가능
  • 이 방식은 shebang이 아니라 POSIX에서 ENOEXEC 발생 시 셸이 /bin/sh로 폴백하는 동작을 이용함
  • 셸은 첫 줄을 명령으로 실행하고, Go 컴파일러는 // 주석으로 인식해 무시함
  • "$0"로 자기 자신 경로를 넘겨 go run이 스크립트를 빌드·실행하고 $@로 인자 전달
  • Go의 강력한 표준 라이브러리와 하위 호환성 보장이 스크립팅 용도에 적합하며, Go 1.x 버전을 사용하는 한 스크립트가 수십 년간 동작 가능
  • Python의 가상 환경, pip/poetry/uv 등 의존성 관리 복잡성을 피할 수 있음

가짜 shebang의 작동 원리

  • shebang(#!)은 execve 시스템 콜을 통해 인터프리터를 지정하는 방식이지만, 이 글에서 소개하는 기법은 shebang이 아님
  • Go 소스 파일 첫 줄에 //usr/local/go/bin/go run "$0" "$@"; exit를 두고 package main 이하에 일반 Go 코드를 두는 형태
    • 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자기 자신을 찾아 빌드·실행하는 형태가 됨
    • "$@"는 1번 인자부터의 위치 인자 확장으로 ./script.go -f flag0 here are some args 같은 호출을 가능하게 함
    • ; exit가 없으면 sh가 Go 파일을 계속 줄 단위로 해석하다가 package 같은 토큰에서 오류가 남

Go가 스크립팅에 적합한 이유

  • Go의 하위 호환성 보장이 핵심 기능으로, Go 1.x를 사용하는 한 작성한 스크립트가 장기간 동작함
  • 잘 발달된 표준 라이브러리와 내장 도구(포맷터, 린터 등)가 별도 설정 없이 제공되어, 스크립트 공유와 이식성이 극대화
    • Python처럼 가상 환경이나 다양한 패키지 매니저(pip, poetry, uv)를 배울 필요 없이 코드 실행 가능
    • Go 생태계의 내장 도구와 IDE 연동으로 .pyprojectpackage.json 없이도 포맷터·린터를 기본적으로 쓸 수 있음
  • 최신 Go만 설치되어 있으면 어떤 OS에서든 수십 년간 실행 가능

다른 컴파일 언어와의 비교

  • Rust는 컴파일 속도가 느리고 표준 라이브러리가 약해 의존성 사용이 필수적이며, 완벽함을 요구해 개발 속도가 느림
  • Java 및 JVM 언어는 이미 JVM 바이트코드 기반 스크립팅 언어가 존재하며, 경량 Kotlin 스크립팅도 대안이 될 수 있음
  • Go는 컴파일 언어 중 스크립팅 용도에 가장 적합한 특성을 보유

gopls 포맷팅 문제와 해결책

  • gopls는 주석 뒤에 공백을 요구(//example// example)하므로 가짜 shebang 라인이 깨짐
  • 공백이 들어가면 // usr/local/go/bin/go가 되어 셸에서 경로로 인식되지 않음
  • 해결책: HN 스레드의 제안으로 // 대신 /**/ 블록 주석 사용
    • /*usr/local/go/bin/go run "$0" "$@"; exit; */ 형태로 작성
    • exit 뒤에 세미콜론(;)이 필수
Hacker News 의견들
  • 작성자가 말한 “pip vs poetry vs uv 신경 안 씀” 부분은 사실 uv가 이 사용 사례를 직접 지원함
    PyPI 의존성까지 포함해서, Python 버전과 uv만 설치되어 있으면 됨
    uv 공식 문서 링크

    • 이보다 더 나은 방법도 있음
      #!/usr/bin/env -S uv run --python 3.14 --script
      이렇게 하면 Python 자체가 설치되어 있지 않아도 uv가 지정된 버전을 내려받아 실행함
    • 나도 그렇게 생각했지만, 비-Python 사용자에게는 아직 직관적이지 않음
      Clojure를 처음 접하면 대부분 Leiningen을 쓰라는 조언을 듣지만, Python은 검색하면 venv, poetry, hatch, uv 등 여러 가지가 나옴
      uv가 점점 대세가 되어가고 있지만 아직은 보편적이지 않음
      예전에 Go를 apt로 설치했다가 버전이 너무 오래돼서 다시 설치했던 경험이 있는데, 그건 훨씬 빨리 해결됐음
      Python의 가상환경 문제는 여전히 복잡함
    • 나는 2019년에 PyFlow로 이 문제를 해결했었음
      Rust로 작성된 OSS 도구로, Python 버전과 venv를 자동 관리함
      pyproject.toml만 설정하고 pyflow main.py를 실행하면 Cargo처럼 의존성을 설치하고 잠그며, 프로젝트에 맞는 Python 버전도 자동으로 맞춰줌
      당시에는 Poetry와 Pipenv가 인기였지만, venv와 버전 관리까지는 부족했음
    • 나도 대부분 uv로 옮겨감
      uv add를 주로 쓰고, 필요할 때만 uv pip를 씀
      하지만 uv pip는 pip의 한계를 그대로 가짐 — 설치 순서에 따라 의존성 해석이 달라짐
      uv pip install dep-adep-b를 설치하는 것과 순서를 바꾸는 것, 혹은 한 번에 설치하는 것이 다 다름
      이는 pip의 문제에 가깝지만, Python 패키지 관리의 혼란은 여전함
    • 사실 Python 버전조차 지정할 필요 없음
      uv가 알아서 내려받음
  • Go는 shebang 지원을 명시적으로 거부했음
    대신 gorun을 쓰는 게 권장됨
    /// 2>/dev/null ; gorun "$0" "$@" ; exit $? 같은 POSIX 트릭으로 실행 가능함
    Nim, Zig, D는 -run 옵션으로 비슷하게 쓸 수 있고, Swift, OCaml, Haskell은 파일을 직접 실행할 수 있음
    관련 토론 링크

    • 작은 스크립트에는 go run 대신 yaegi 인터프리터가 더 나을 수도 있음
      yaegi GitHub
  • “pip, poetry, uv 차이를 알고 싶지 않다, 그냥 코드만 실행하고 싶다”는 글은 결국 기술 숙련도 문제
    uv runPEP 723이 이미 모든 문제를 해결했음

    • 맞음, 하지만 uv run이 나오기까지 너무 오래 걸렸음
      20년 넘게 Python을 써왔지만, 외부 패키지나 venv가 있는 코드베이스는 항상 두려웠음
      uv run 덕분에 회사 프로젝트는 모두 옮겼지만, 개인 프로젝트는 이미 Go로 넘어감
      장기적으로는 정적 타입 언어를 선호함
    • 오래된 언어라면 결국 경쟁 라이브러리를 배우게 됨
    • 이건 UX 문제
      사용자는 그냥 프로그램이 실행되길 원함
      uv run과 PEP 723이 문제를 해결했지만, uv를 알아야 한다는 점에서 여전히 진입장벽이 있음
      uv가 공식 기본 도구가 아닌 이상, 많은 사용자가 Python을 떠날 것임
  • 정말 천재적인 아이디어라고 생각함
    하지만 스크립팅은 배포용 소프트웨어와는 다른 인체공학적 감각이 필요함
    bash는 즉흥적이고, Go는 제품화에 적합하며, Python은 그 중간쯤, Ruby는 bash에 가깝고, Rust는 Go 쪽임
    스크립트는 OS 명령어를 빠르게 조합해 일회성 작업을 처리할 때 유용함
    Go는 그런 즉흥성이 부족함

    • 나도 Python이 “중간쯤”이라는 말에 공감함
      Debian에서 간단한 gtk 앱을 uv로 실행하려 했는데, 의존성은 다 맞는데도 실행이 안 되고 결국 Core Dump
      Python을 새로 시도할 때마다 이런 일이 반복됨
      Go는 장황하지만, 한 번 컴파일되면 그냥 동작함
    • 나도 비슷하게 느낀다
      핵심은 한 파일로 끝낼 수 있느냐임
      Go로도 500줄짜리 스크립트는 가능하지만, 언어 자체가 여러 파일과 모듈을 전제로 함
      bang-line이 안 되는 것도 그런 이유임
      어차피 go run이 임시 바이너리를 만드는 구조라면, 그냥 빌드해서 /usr/local/bin에 넣는 게 낫다고 봄
    • bash가 OS 명령어에 더 가깝다는 말은 오해임
      bash도 Python만큼이나 OS 위의 추상 계층일 뿐, 단지 기본 셸이라 그렇게 느껴질 뿐임
    • LLM이 코드를 대신 수정해주는 시대라면, 작성 인체공학보다 가독성이 더 중요해질 수도 있음
      특히 LLM이 쓴 코드를 사람이 읽기 쉽게 만드는 방향으로
  • Python을 처음 접하는 사용자가 pip, poetry, uv 차이를 몰라도 된다는 점에는 동의함
    하지만 이런 주제로 글을 쓰는 블로거라면 최소한 uv가 문제를 해결한다는 사실 정도는 알아야 함
    무지한 비판은 설득력이 없음

    • uv가 Go처럼 “write once, run anywhere”를 해결하냐는 질문이 있음
      나도 uv의 개념을 완전히 이해하지 못했기에 궁금함
  • 나는 Python으로 스크립트 짜는 걸 좋아함
    빠르게 작업할 수 있고, 타입이나 메모리 걱정 없이 단순한 일을 처리하기 좋음
    하지만 대규모 애플리케이션에는 쓰고 싶지 않음

    • 나도 Python 스크립팅은 좋아하지만, 남의 스크립트를 설치하는 건 싫음
    • 이건 Linux 중심의 접근임
      대부분 시스템에 기본 Python이 있어서 간단한 스크립트엔 충분함
      Go를 설치해야 하는 점을 생각하면, 차라리 uv로 Python을 쓰는 게 낫다고 봄
      글쓴이도 “약간 트롤링으로 시작했다”고 했듯, 결국 Go 선호의 문제임
    • JS도 스크립트 언어로 나쁘지 않다고 생각함
      node bla.js면 끝임
    • 타입은 항상 신경 써야 함
      함수가 뭘 반환하는지 알아야 하고, 언어를 잘 알면 기본 타입은 기억으로 처리함
      이건 정적 타입 언어도 마찬가지임
    • Python은 개발자에게는 훌륭하지만, 배포나 통합에는 악몽
      다른 사람을 고려한다면 Python으로 배포용 코드를 쓰지 말아야 함
  • 기대했던 건 Python 비판이었는데, 오히려 유용한 팁이었음
    //를 주석으로 쓰는 언어라면 이 트릭을 응용할 수 있음
    C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL 등 가능함
    특히 GLSL로 단일 파일 그래픽 데모를 만드는 게 흥미로움
    Shadertoy 예시
    C에서는 블록 주석을 이용해 /*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */ 같은 방식으로도 가능함

    • Swift에는 외부 의존성을 가진 스크립트를 실행할 수 있는 swift-sh 프로젝트가 있음
      Swift용 uv 같은 개념임
      Swift는 shebang도 공식 지원함
    • C/C++에서는 #!을 직접 써도 됨
      예전 TCC 시절에 “C 스크립팅”으로 이런 방식을 썼음
      큰 프로젝트에서는 빌드 스크립트가 매니페스트를 읽고 빌드 후 실행하는 구조였음
      하지만 환경 제어가 어려워서 실무에는 부적합함
    • Rust는 이런 꼼수를 쓸 필요 없음
      shebang을 직접 지원함
  • 인체공학적인 언어를 원한다면 .NET 10의 “run file directly” 기능도 있음
    shebang을 지원하고, 스크립트 내에서 패키지를 자동 설치함
    #:sdk 지시어로 웹앱도 바로 실행 가능함

    • 나도 오늘 처음 이 기능으로 C# 스크립트를 작성했는데, 꽤 괜찮은 경험이었음
      다만 AOT 컴파일은 아직 거칠음
  • 처음엔 Python 비판일 줄 알았는데, 오히려 언어 생태계의 방향성을 생각하게 됨
    ML이 Python에 묶인 건 큰 실수라고 봄
    느리고, 타입 시스템이 불편하며, 배포가 어렵기 때문임
    이제는 TypeScript, Go, Rust 같은 대안을 고려해야 함

    • 동의함
      다만 ML이 Python을 택한 이유는 C 기반 FFI 때문임
      NodeJS나 Rust, Go는 FFI가 약함
      Python은 여기서 강점을 가짐
      이상적인 건 Python처럼 간단하지만 더 나은 타입 시스템과 배포 체계를 가진 언어임
    • TypeScript로 대체하자는 건 동의 못함
      JS 생태계에서 나온 언어로 Python을 대체하고 싶진 않음
    • ML이 Python으로 간 건 시장 압력 때문임
      Lisp이나 Lua(Torch)가 더 적합했지만, 단순함 때문에 Python이 선택됨
      나도 Lisp 기반의 ML 프레임워크를 개발 중이지만, 채택은 어려울 것 같음
    • Python의 의존성 지옥은 여전히 심각함
      버전 호환성 문제, semver 부재, 불안정한 생태계 등으로 인해 JS보다 뒤처진 느낌임
      JS/Node는 지난 10년간 성숙했지만 Python은 여전히 2012년에 머물러 있음
      ML이 Python에 표준화된 건 정말 아쉬움
    • 나는 단순하고 표현력 있으면서 강타입 + 네이티브 컴파일되는 언어를 원함
      CLI 도구를 만들 때 Python보다 Go가 훨씬 빠름
      LOC 차이 때문에 Python으로 돌아오긴 했지만, 실행할 때마다 Go가 그립음
      아마 OCaml이 이상적이지만, 구식 툴링이 부담스러움
  • Go 스크립트의 문제는 첫 줄에 공백이 없어야 한다는 점임
    gopls가 자동 포맷팅을 강제하기 때문임
    CI에서도 포맷 일관성을 유지해야 하므로, 이건 실무적으로 중요함
    하지만 더 큰 문제는 go.mod를 쓸 수 없다는 것
    즉, 의존성 버전을 지정할 수 없어 호환성 보장이 약해짐

    • 그래도 메이저 버전은 import 경로로 잠기기 때문에 기본적으로는 호환됨
    • 이건 언어/런타임 수준의 호환성 문제이지, 의존성 문제는 아님