💪

Goのプラクティスまとめ: プロジェクトを始める

に公開

Goのプラクティスまとめ: プロジェクトを始める

筆者がGoを使い始めた時に分からなくて困ったこととか最初から知りたかったようなことを色々まとめる一連の記事です。

以前書いた記事のrevisited版です。話の粒度を細かくしてあとから記事を差し込みやすくします。

他の記事へのリンク集

プロジェクトを始める

新しいプログラミング言語、フレームワーク、ライブラリー、ツール、etcを始めるとき筆者にとってよく障害となるのは「プロジェクトを始めるまでの方法がわからない」ということです。

そこで、この記事ではGoそのものの紹介、SDKのインストール、エディターのセットアップ、プロジェクトの開始方法(=moduleの作成方法)、さらにprivate repositoryで管理されるgo moduleをインポートできるようにする方法、タスクランナーなどについてまとめます。

前提知識

  • ほか言語での開発経験
    • pythonNode.jsなどを引き合いに出すことがあります。
    • ある程度ソフトウェア開発における常識感を読者が持っているのを前提とした説明をします。
      • 誰でもわかるように書くと論文みたいになって長くなる傾向がありますし、裏取りの手間が大きくなりすぎるためです。
      • 「常識感」についても共有されていないと曖昧性が増すため筆者の知識のバックグラウンドも簡単に書きます。

環境

win11のwsl2インスタンス内で動作させます。ほかの環境でも動く気がしますが、以下が前提となります。

> wsl --version
WSL バージョン: 2.4.12.0
カーネル バージョン: 5.15.167.4-1
WSLg バージョン: 1.0.65
MSRDC バージョン: 1.2.5716
Direct3D バージョン: 1.611.1-81528511
DXCore バージョン: 10.0.26100.1-240331-1435.ge-release
Windows バージョン: 10.0.26100.3775

Goは現在(2025-05-05)の最新です。

$ go version
go version go1.24.2 linux/amd64

Goは後方互換性をかなり気にするためサンプルコードや述べられていることは以降のバージョンでも(何ならこれよりの前のバージョンでも)基本的に一緒ですが、細かいところで改善が入ったりするのでそれを踏まえて読んでください。

snippetのconvention

  • shellコマンドの羅列の場合コピペしやすさを優先して特になにもprefixをつけずにコマンドを羅列します。
  • ただし、コマンド実行結果をsnipet内の併記したい場合は、コマンドは$ でprefixされます。
  • # から始まる行はコメントです。

筆者のバックグラウンド

学生時代に

  • Verilog(HDL)でAltera製のFPGAの回路を記述していた
  • C++(Visual Studio Community 2019だったと思う)を使ってセンサーから値を読み込んで計算を行うプログラムを作っていた
    • 四元数で回転を計算するのにEigenを使っていました。
    • GUIをつけるのにMFCを使っていました。

FPGAを使ったプロジェクトはとん挫したので遊びみたいなものです。
機械や制御を専門とする学徒であったためこの時点ではソフトウェアに詳しくはありませんでした。とくにC++に関しては研究室に置いてあった古い本を読みながらやったので当時からしても古い書き方をしていたと思います。

社会人になってから

  • Node.js(TypeScript):
    • 業務アプリの一部みたいなやつ、APIサーバーとかを書いてました。
    • イベントループがあるのがいいなあと思ったんですが、仕様の過渡期とかがつらくて離れました。
      • Node.jscommonjs module -> ECMA module
      • Node.js stream -> WebStream
      • -> fetch などなど
    • もうすぐ移行が終わりそうなので楽しみにしてます。
  • ちょっとだけpython:
    • 業務改善のためのツールに使用してました。Excelをいじくるやつです。実行ファイルに固めてくれと言われてこりゃしんどいわとなってのちにGoで書きなおしています。
    • pythonのasyncについて教えるためにasync周りのCPythonの実装を読んでます。おもしろいです。
  • Rust:
    • C/C++で書かれたライブラリのbindingを書いて使ってた程度であんまりメインで使ってるわけではないです。
    • PDFiumへのbindingを書いてました。pdfium_rsをフォークして拡張する形でやってました。楽しかったなあ。
    • denoのruntimeがRust(tokio)なので必然的に読む機会は多いです。
  • Go: 仕事でも趣味でも使っています。APIサーバーを書いたり、Docker APIを通じてコンテナ状態をいじったり、画像をいじくったりいろいろやってます。

別に書かないですが

  • Java: 学生時代Swing JAVAでUIを作る講義がありました。Elasticsearchのソースを読んだりしてます。
  • C: linux kernelとかbusyboxとかの詳細がわからなくて困ることがあるので読まざるを得ません。プロジェクトによってはマクロでオブジェクト指向みたいなことしてて毎回ビビらされます。QEMU/virsh, OpenSSL,などなど重要なツールはどれもCで書かれている気がします。が、最近はRustに移行するものも増えてきましたね。

少し特殊な環境で動くソフトウェアを書くためデータベース周りの話にあまり明るくなかったりします。
手元でスクリプティングを行う場合はもっぱらneovimLuadeno, Goのいずれかで行っています。

基本: Goとは

特徴

https://go.dev/doc/

The Go programming language is an open source project to make programmers more productive.

Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.

これよりもう少し説明すると、Go

  • C系統の文法で
  • 静的型付けで
  • 言語に組み込まれたgoroutine, いわゆるGreen Threadの機能があります。
    • これを呼び出すにはgoキーワードの後に関数やmethodの呼び出しを書くだけです。簡単です。
    • goroutineのランタイムがOS ThreadをCPU個数と同数(正確には違う)作っておいて、それらの上でgoroutineをスケジュールします。
    • CPUと同数個のOS Threadを作っておいて、そのうえでタスクを動かすのはNode.jsも似たようなコンセプトを持っていますが、goroutineは普通のOS Threadかのようにユーザーには見えるという違いがあります。
    • Rustにはasync/await構文があり、tokioなどのランタイムを用いると似たようなことができます(denotokioを使用しています)が、こちらは(async fnは)stackless statemachineになる方式、goroutineはそれぞれがstackを持っていますので違いがあります。
    • cooperativeなタスクの切り替えしかできないかと思いきやGo 1.14からpreemptiveな切り替えにも対応しています。
  • GC(Garbage Collector)があり、手動でのメモリ確保・解放はほぼやることはありません。
  • 言語仕様が簡潔で、言語としてサポートされた構文は多くありません。
    • spec#Keywordsより、keywordは25個とほかの言語と比べて少ないです。
    • 例えばほかの言語でよくあるwhileはなく、代わりにcondition部分のないfor { /* do anything */ }を用います。
  • コンパイルが遅くならないように気が遣われています。
    • 機能も構文もあまりコンパイルが遅くならないように気遣って追加されるようです。proposalを読んでるとコンパイルの速度が遅くならない方式だからこの方法を採用する、みたいな言及があります。
    • go run ./path/to/mainを実行すると、毎回ビルドしてから実行するんですが、こうしても気にならないほどにはコンパイルは速いです。
  • classや継承はなく、interfaceによる抽象化を行います。
    • interfaceは特定のmethod setを実装する型なら何でも代入できる型という意味になります。
    • interface{}(なんのmethodも指定しないinterface)にはあらゆる型の変数を代入可能です。上記のfeels like a dynamically typed, interpreted languageの部分はこのこともさしているのだと思います。
  • methodは、任意の型をベースとした型を定義し、それに関連した関数として実装します。
    • type A struct { Foo string; Bar int }type B stringのように、別の型をベースとして型を定義できます
    • もちろん、type C Aも可能です。
    • func (a A) MethodName() {}という構文でmethodは定義できます。ここでいうaがほかの言語でいうselfとかthisです。
      • 同じpackage内なら複数のファイルに分散して定義したりできます。
  • method、関数、closureはすべて区別なく変数や引数に代入可能です。
  • 組み込み型としてarray([5]T)、slice([]T) = 動的にサイズが変更できるarray;ほかの言語のvectorに近いもの、map(map[K]V) = hash map,chan(chan T) = channel(goroutine-safeな通信ができる)があってこれらはfor-range構文が対応していたりと特別扱いされます。
  • moduleシステムが組み込まれています。
    • moduleの公開には特別な処理は必要ありません。githubのようなVCS(Version Control System)で公開し、Go moduleの名前をhttps://抜きのURLにすればそれで公開できます。
  • pointerは存在しますが、pointer arithmeticは(unsafeを使わない限り)できません
  • 非常に簡単に別のCPUアーキテクチャ、OS向けのビルドを行うことができます。
    • ただしCGO(FFI)を使わない場合
  • 非常に簡単にstaticにビルドできます。
    • ただしCGO(FFI)を使わない場合

よくいろいろないといわれますが

  • genericsはGo 1.18で実装されました
  • iteratorはGo 1.23で実装されました
  • ?構文はdicussionに入ってるので1~2年以内に実装されるかも!
    • The Go Blog: [ On | No ] syntactic support for error handlingで説明される通り、コンセンサスを得られるproposalがないためそもそもerror handling周りの構文変更自体しばらくしない(proposalが出ても即閉じられる)という決定になったらしいです。ないならないでよし!

A Tour of Go

https://go.dev/tour/welcome/1

Goの基本的な文法、機能などのトピックはここに書いてあります。
インタラクティブなコードスニペットと簡単なエクササイズがあり、これさえこなせばとりあえず開発は始められます。
以後の文章はA Tour of Goをすべてこなしたことを前提とします。

慣れてない頃に、syntax highlightのかからないwebページでコードを書くのはきついと思いますが、軽く調べた限り任意のエディターで実行する簡単な方法はなさそうです。

筆者の記憶にある限り筆者は合計6時間ぐらいですべて終わりました(当時はまだgenerics実装前だったので今はもう少し長い)。
時間のほとんどは構文エラーがよくわかんなくて費やされたのでローカルのエディターにコピーして書いてコピーしなおして実行すればもっと早く終わるかもしれません。

Go by Example

https://gobyexample.com/

コード例とともに解説がされます。
項目数が多く、知らないstdmoduleを使う部分をきちんと理解しようとすると時間がかかると思います。
手が空いたら少しずつ読むのがいいのではないでしょうか。

公式読み物系

公式が出している読み物集。全部英語です。
内容が古くなりつつあるため、始めたてで読むべきかは微妙ですが、古くなっても参考になる部分が大いにあります。

そのほかにもいろいろなトピックが以下に掲載されています。

https://go.dev/doc/

The Go Programming Language Specification

https://go.dev/ref/spec

言語仕様ですが割と短めなのでそのうち読んでおいたほうがよいでしょう。

Std library

https://pkg.go.dev/std

standard libraryです。

HTTPなどで動作するサーバープログラムを作るのに大体必要な機能がそろっています。
できれば開発に着手する前にさっくりどういう機能がstdとして存在するかを把握しておくほうがよいでしょう。

CGOSWIGへの言及があるなど、読んどけばよかった系の文章が意外なほどたくさん書いてあります。

Sub-repositories

https://pkg.go.dev/golang.org/x

Sub-repositoriesです。

説明のとおり、Go Projectの一環ですがstd libほど厳密なバージョン管理がされていません。
std libに入ると厳密な後方互換性の約束を守る必要があります。
そのため変更の可能性が高かったり、stdに入れるほどの重要度がないものがこちらにあるというコンセプトのはずです。

  • ここで先に実装されてからstdに昇格されたり(maps, slicesなど)、
  • stdがFrozenなので代わりにこちらのものを使うべきだったり(syscallの代わりにgolang.org/x/sys)
  • 1ファイルにバンドルされてstdに組み込まれていたり(golang.org/x/net/http2)
  • 古いGoでも利用できるようにstdへの追加がこちらにも入れられる(encoding/json/v2など)

することもあります。

golang/example

https://github.com/golang/example

公式でメンテされているexample集。
go/types周りの話など結構アップデートされている部分もある。

SDKのインストール

公式の手順に従い、各OS環境に合わせてGoをインストールしましょう。

https://go.dev/doc/install

ダウンロード

linux/amd64の場合、

VER=1.24.2
mkdir -p /tmp/go-download
curl -L https://go.dev/dl/go${VER}.linux-amd64.tar.gz -o /tmp/go-download/dist.tar.gz

一応チェックサムを確認しておいたほうがいいかもしれません。

# checksumの値はダウンロードページから確認できる。
CHECKSUM=68097bd680839cbc9d464a0edce4f7c333975e27a90246890e9f1078c7e702ad
ACTUAL=$(sha256sum /tmp/go-download/dist.tar.gz | awk '{print $1}')
[[ $CHECKSUM = $ACTUAL ]]; echo $?

bashじゃないと動かないかも。一致してれば0がprintされます。

/usr/local以下に入れる場合

公式は/usr/local以下に入れる方法を案内しています。

sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xf /tmp/go-download/dist.tar.gz

~/.local以下に入れる場合

共有サーバーでユーザーにsudo権限をつけたくないときなど、$HOME以下へインストールしたいこともあると思います。
~/.local以下にインストールしても全く問題なく動作します。

rm -rf ~/.local/go && tar -C ~/.local -xf /tmp/go-download/dist.tar.gz

以降は~/.local/goに入れてある前提で書かれます。

PATHを通す

環境変数を設定。使っているOS/terminalに合わせた方法で設定してください。

筆者は以下のようなbashscriptを組んでこれを.bashrcから呼び出しています。

#!/bin/bash

gobin=~/.local/go/bin/go

export PATH=$($gobin env GOROOT)/bin:$PATH

if [[ -n $($gobin env GOBIN) ]]; then
    export PATH=$($gobin env GOBIN):$PATH
else
    export PATH=$($gobin env GOPATH)/bin:$PATH
fi

Go Modules Reference#go-installより、go installでビルドされたバイナリは$GOBIN以下、もしくは$GOPATH/binに保存されます。
そちらにもPATHを通しておけばコマンドとして利用できるようになります。

例としてlazygitgo installしてみましょう。

go install github.com/jesseduffield/lazygit@latest
which lazygit
# 筆者の環境では`$(go env GOPATH)/bin/lazygit`が表示されます。

環境変数GOBINもしくはgo env -w GOBIN=/path/to/gobinGOBINが設定されている場合はそこ以下にバイナリが保存されます。

エディタのセットアップ

エディタは個人の好みともろもろを合わせて好きに選べばいいと思います。
ただよく聞く+goのsurveyの上位3位は以下の3通りです。

設定は大して難しくないので省略します。案内に従ってください。
例外としてvim,neovimの言語サーバーの設定は難しいと思いますが、これらをあえて選ぶ人物は自分で調べる能力が高いでしょうからやはり省略します。

goのsurveyはself-selection, vscodeGo extensionからの誘導, GoLandからの誘導の経路があるのでバイアスがかかっているのは間違いないと思いますが感覚的にこの3つが上記の順で多いのは間違いなさそうに感じます。

Go moduleの作成

  • Goではプログラムは1つまたは複数のGo moduleで構成されます。
    • Go moduleGo 1.11で導入されましたが、今日においてGo moduleを用いずGoで開発することはほぼあり得ません。
  • go mod init <<module-name>>でmoduleを初期化します。
    • go.modが生成されます。これがあることでgo toolgo moduleとして認識されます。
    • <<module-name>>はVCS repositoryのURIからprotocol schemeを抜いたものにするとよいです。
      • こうすると、go get <<module-name>>でほかのgo moduleからimport可能になります。
  • とりあえずGo moduleを作る
    • 実行ファイルを作りたくても、
    • ライブラリを作りたくても
    • 実行ファイルにもなるし、ライブラリとして公開されるものでも
  • moduleはさらにpackageと呼ばれる単位に分割されます。
  • 1 directory = 1 packageです
    • 例外的にmain以外のpackageは_test suffixをつけた(e.g. pkgに対してpkg_test)テスト用packageを定義することができる
  • packageはnamespaceを共有します。
    • 複数ファイルにわたって同じ変数、関数にアクセスできます
    • ほかの言語、例えばRustでは1つのファイルが1つのmoduleであり、ファイル内でもmoduleを定義することができますが、逆にGoではこのように任意に分割する方法はありません。
  • main packageが実行ファイルとしてビルドできる
    • main packageのmain関数がエントリーポイントになる
    • main packageは任意のパスに任意の個数用意できる。
  • go build ./path/to/main/package/でディレクトリ指定でビルドする
    • 必ず./から始まるパスを指定する。ないとstd libraryを指定しているとgo toolに思われる。
  • main以外のpackageはfully-qualified path(<<module-name>>/path/to/directory)でimportできる。
    • 相対パスを用いることもできるが、ほぼされることがないためしないほうが良いのではないかと思う。
  • 外部のmoduleはgo getで取得することができる。

事前にgit(VCS)でrepositoryを作成しておく

手順そのものは説明しませんが、以下の手順はgithubgitlabでrepositoryが存在していることを想定します。

VCS(Version Control System)は, コンピュータファイルのバージョンを管理するシステムのことです。
代表的なものはgitsvn,mercurialあたりだと思います。
この記事ではgitのみを取り扱います(筆者がほか二つのことをほぼまったく知らないからです)

gitは、VCSを構築するためのサーバーおよびクライアントプログラムです。サーバーとして直接使うことはほとんどないかもしれません。
現在ではgitサーバーはgithubというwebサービスを利用するか、 セルフホストすることも可能なgitlab、あるいはgitbucketなどを使うのが一般的だと思います。(この3つがリストされてるのは単に筆者が使ったことあるやつ3種っていうだけです)

先にローカルでrepositoryを作成してあとからremote上に作成する方法もあるはずですが、この説明ではremoteがすでに存在していることを前提とします。

Go moduleの初期化

VCSで作成したrepositoryをローカルにcloneします。

clone先はどこでもいいですが、個人的なおすすめは<<uri>>からhttps://などのprotocol schemeを外したパスで~/以下にディレクトリを切り、そこにcloneする管理方法です。

uri=<<uri>>
dest=$HOME/gitrepo/$(echo $uri | sed 's/\.git$//' | sed 's/^git@//' | sed 's/^https\:\/\///')
mkdir -p $dest
git clone $uri $dest
# clone先に移動しておきます
pushd $dest

go mod init <<module-name>>Go moduleに必要なファイルを作成します

go mod init <<module-name>>

<<module-name>>は基本的に上記<<uri>>からprotocol schemeを抜いたものにするとよいです。
そうするとgo get <<module-name>>でこのmoduleを別のGo moduleへ導入できるためです。

例えば、VCSのuriがhttps://github.com/ngicks/go-example-basics-revisitedである場合、

go mod init github.com/ngicks/go-example-basics-revisited

で作成し、VCSにソースをプッシュすると

go get github.com/ngicks/go-example-basics-revisited

で別moduleから導入、参照できます。

local onlyのmodule

ローカルオンリーなつもりならおおむね<<module-name>>はなんでもよいはずです。

go mod init whatever-whatever

筆者が知る限りほかのmoduleからgo getできなくなる以外の違いはないです。

ただし、How to write codeでも勧められる通り、なるだけ公開される前提でmoduleを作っておいたほうがよいでしょう。

private VCSかつサブグループを使用する場合.gitなどでmodule nameをsuffixしておく

ただし、private VCS(=URIがgo toolに対して既知でない)でサブグループを作成し、サブグループの中でソースを管理する場合、<<module-name>>はvcsのsuffixを加えておかないとgo get時に失敗するかもしれません。

つまり上記と同じ例で行くと

# 架空のURLを扱うのでexample.comに変えてあります!
go mod init example.com/ngicks/subgroup/go-example-basics-revisited.git

とする必要があるということです。

なぜかというと

  • private repositoryを参照できるgo module proxyサーバーが存在しないとき、go toolgit ls-remoteなどのvcsコマンドを直接実行します。
    • これはdirect modeと呼ばれます。
  • https://pkg.go.dev/cmd/go#hdr-Remote_import_paths より、
    • module nameにVCS suffix(e.g. .git, .hg, .svn) がついていると、このsuffixまでをmodule root pathとする
    • ない場合、go toolに渡されたパスに対して?go-get=1付きでHTTP GETを行い、responseからmodule root pathを得ます。
    • (余談ですがログを見る限りVCS suffixがついていても?go-get=1付きのHTTP GET自体はされます)
  • go toolに渡されるパスはmodule root pathとは限らないため、たとえgo get module/root/pathをしたとしても、subgroupが含まれているとパスの探索が起こります。
    • e.g.
      • go install: <<module-name>>/cmd/command-nameのようにsub-packageにmain packageを作ることが多い。
      • mono-repo: 1つのVCS repositoryに対して複数のgo moduleをホストすることができますのでmodule root = gitなどでcloneする対象とも限りません。
  • (少なくとも)gitlabでは(おそらく)あらゆるパスに対して?go-get=1 query paramをつけたHTTP GETが成功します
    • 返ってくる内容はgo toolの期待に反してmodule root pathではなく、アクセスされたURLのパスをオウム返しするだけです。
    • おそらくセキュリティーのためです(後述)。そのためおそらくどのVCSでもこうなっているんじゃないでしょうか

というのが理由です。

Go 1.24まで、.netrcを用いる以外にgo toolにcredentialを渡す方法がなく、そのため?go-get=1は認証情報なしでリクエストされることがほとんどだったと思われます。
少なくとも筆者の環境では.netrcを作成していませんがprivate repositoryからgo getが成功していました。ということは認証なしで?go-get=1付きのリクエストはとりあえず成功するようにできていたんだと思います。404とか403みたいにエラー種がパスによって変わるとどういうパス構成なのかが外部からばれてしまうため、"正しい"内容を認証していない状態で返すわけにはいきませんので、こういう挙動になっていたのでしょう。
direct modeでgitコマンドを直接用いる際にはgitコマンドのcredentialがそのまま利用されますから、ここは広く用いられる方法でcredentialを渡すことができます。
後方互換性のことを考えるとこの挙動が変わることはまずない気がします。

https://gitlab.com/gitlab-org/api?go-get=1 はPublicですが、これも与えられたパスをオウム返しする挙動であるのでPublicであってもうまく動かないと思われます。このURLが指し示す先はサブグループなので500番台とか400とかを返すのが正しい挙動に思えますね。

もし仮にmodule nameにVCS suffixをつけたくないとしたら、好ましい解決方法は

  • サブグループを使わない
  • vanity import pathを返すサーバーを運用する
  • private repositoryを参照できるgo module proxyを運用する

だと思います。
筆者はVCS suffixをつける方法に甘んじているためすべて試していないことに注意願います。

  • サブグループを使わない:
    • 単純ですが、サブグループを使わないだけでも解決します。
    • https://github.com/golang/go/blob/go1.24.2/src/cmd/go/internal/vcs/vcs.go#L1523-L1585 などより、VCS suffixがない場合、<<host>>/<<user>>/<<repository>>というフォーマットとして解釈され、このパスにまず?go-get=1付きのHTTP GETが試みられます。前述のとおり少なくともgitlabではこれは成功するため意図通りに動作すると思われます。
  • vanity import pathを返すサーバーを運用する:
    • 例えばgo.uber.org/zapは実際にはgitubでホストされています。
    • curl https://go.uber.org/zap/zapcore?go-get=1を実行するとわかりますが、<meta name="go-import" content="vanity/module/path VCS https://path/to/VCS">が返ってきます。
    • このようにmodule pathと実際にソースコードがホストされるURLが違う時、module pathをvanity import pathなどと呼ぶのが通例のようです。
    • ?go-get=1で正しい内容が返りさえすれば(=module root pathが正しく取れれば)あとは成功するでしょうから、これでもうまくいくと思われます。
    • (余談ですが、mono-repoかつvanity import pathを用いたい場合module proxyを用いなければうまく動作しない気がしています。)
  • go module proxyを運用する:
    • 最も正道で最も大変な方法と思われます。
    • module proxyGOPROXY protocolを実装したHTTPサーバーです。
    • 見たところ実装自体は難しくなさそうですが、これのために1つ運用するサーバーを増やすというのもどうなのかなあという気持ちもあります。
    • github.com/goproxy/goproxyがgoでgo module proxyを実装しているのでこれを用いるか、
    • kubernetes clusterを動作させているならArtifactHUBから探してHelmで導入するなどするとよいかもしれないです。
    • このmodule proxy server自体にauthが必要ですのでGOAUTHを各clientに設定してもらうか、イントラからしかアクセスできないようにするかする必要があります。これはこれで大変ですね。

module proxyを運用したい別の理由があるなら話は違いますが、VCS suffixをつけてしまうのが一番楽です。

とりあえずmainを作ってビルドして実行してみる

先ほどクローンしたディレクトリで、以下のようにファイルを作成し、Go moduleを初期化しましょう

以下の手順ではgit repositoryの直下じゃなくてサブディレクトリにmoduleを作っています。これはこの記事向けのスニペットをまとめて同じrepositoryに置きたい筆者の都合です。
なので読者はパスはいい感じに読み替えて都合のいいパスで実行してください。

mkdir starting-projects
cd starting-projects
go mod init github.com/ngicks/go-example-basics-revisited/starting-projects

go mod init実行後に以下のファイルが作成されたと思います

go.mod
module github.com/ngicks/go-example-basics-revisited/starting-projects

go 1.24.2

このファイルが、go module

  • 名前(というかパス)
  • version
  • toolchain
  • 依存するほかのgo module

などを記録するファイルとなります。
pyproject.tomlpackage.jsondeno.jsonなどと近しいものです。

このファイルはgo getgo mod tidyなどのコマンドに編集してもらうことになるので、手で編集することは少ないです。

go versionのfix release(1.24.2の末尾の.2)が0以外だと少々具合が悪いので編集します。

go mod edit -go=1.24.0

するとgo.modの内容は以下のように変更されます。

go.mod
module github.com/ngicks/go-example-basics-revisited/starting-projects

-go 1.24.2
+go 1.24.0

Goのmajor release(Go 1.23Go 1.24のような)はAPI追加、構文の追加、たまにエッジケースの挙動が破壊的に変更されますから、これは重要な観点です。他方、fix releaseはセキュリティーにかかわるfix以外では挙動の変更は起こらないことになっています。
別に動作するにもかかわらずfix releaseが古いgo moduleからgo getできなくなるため、基本的にはfix releaseは.0を指定しておくほうが良いのではないかと思います。
std libraryはビルドするときのtoolchainのものが使われるため、ビルドする側の設定次第で1.24.2でもビルドできますのでgo.modでは常に.0を指定していても問題ないはずです。

エントリーポイントを作成します。

mkdir -p cmd/example
touch cmd/example/main.go

ファイルの中身を以下のようにします。

cmd/example/main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello world")
}

main packageのmain関数がエントリーポイントとなります。
func init() {}が定義されているとそちらが先に実行されるのでmainが必ず最初に実行されるというわけではありません。

以下のコマンドでビルドすることができます。

go build ./cmd/example

linux/amd64で実行すると./exampleが出力されます。
windowsだと./example.exeになります。

もしくは以下のコマンドで実行します

$ go run ./cmd/example
Hello world

go runはOS依存のtmpディレクトリにビルドして実行するショートハンド的コマンドで、毎回ビルドしてしまうので複数回実行したい場合はgo buildしたほうが良いことが多いでしょう。

ちなみに、以下ではダメです。

$ go build cmd/example
package cmd/example is not in std (~/.local/go/src/cmd/example)
$ go help packages
...
An import path that is a rooted path or that begins with
a . or .. element is interpreted as a file system path and
denotes the package in that directory.

Otherwise, the import path P denotes the package found in
the directory DIR/src/P for some DIR listed in the GOPATH
environment variable (For more details see: 'go help gopath').
...

とあるように、/C:\...から始まらないパスは$(go env GOPATH)以下にあるかのように解決されてしまうからです。
(前述した表現ではstdであるかのように解決される、と述べていますが、これはGOPATHをいじることはもうないから、すなわちGOPATH=stdと実質なっているからです。)

以下の場合はエラーなく実行できますが、packageが複数のファイルを含む場合うまくビルドできないことを筆者は確認しています。

$ go run cmd/example/main.go
Hello world

つまり、cmd/example以下にファイルを足してcmd/example/main.goがそれを参照するようにすると

cat << EOF > cmd/example/other.go
package main

var Foo = "foo"
EOF
cmd/example/main.go
package main

import "fmt"

func main() {
-  fmt.Println("Hello world")
+  fmt.Println("Hello world", Foo)
}

以下のような感じでエラーを吐きます。

$ go run ./cmd/example/main.go
command-line-arguments
cmd/example/main.go:6:29: undefined: Foo

実はファイルリストだったら実行できるんですが

$ go run ./cmd/example/main.go ./cmd/example/other.go
Hello world foo

ファイルが増えると困りますよね?
なのでファイルパスじゃなくてpacakge pathで指定するとよいでしょう。

相対パスでビルドを行う場合は./を必ず含めて、directory名で指定するとよいでしょう。

packageを分ける

前述のとおり、moduleは複数のpackageに分割されます。
Go moduleでは1 directory(フォルダー) = 1 packageとなります。

  • 1つのdirectoryは1つのpackageしか持てません。
    • ただし例外としてmain package以外については、package nameに_testというsuffixをつけてテスト用の別packageを同一directory内に定義できます(e.g. pkgに対してpkg_test)。
      • _test packageは別のpackageなので、元のpackageとnamespaceを共有しません。そのためexportされたシンボルにしかアクセスできません。
      • 何かの都合でテスト内で循環参照が生じてしまうようなときに_testを定義してそれを回避するのが主な使い道になります。
  • Goの慣習とGo teamのおすすめ的には、packageは関心をもとに分割するのが良いとされています。
    • まずはpackageは分割せず、同一package内にすべて定義し、不都合が生じ始めたら分割したらよい、と言われています。
    • もちろんもとから関心が別れる点が明確であれば先だってpackageを分割しておいても特段問題はないと思います。
  • packageとdirecotryの名前は一致しているのが望ましいです。
    • これは、import時にデフォルトではpackageで指定される名前でそのpackageにアクセスできるようになるため、import path(= directoryの名前)とpackageの名前が不一致だとものすごく読みにくくなってしまうからです。
      • gopls(Goの言語サーバー)が機能していると自動的にimportが追加されたりしますが、そのさいにpackageの名前が指定されるように修正されます。(i.e. import name "path/to/pkg")
    • 例外的にpackageの名前がdirectoryの名前のsuffixである場合は問題なしとされるようです
      • 上記のgoplsの修正が起こらない。
      • semanticTokensの設定を有効にしていると、packageの名前部分だけ色が変わるのでわかるようになります。
  • 慣習的にpackageの名前は1語で短いものが良いとされます。
    • 長い名前を与えないといけないということはpackage階層設計がうまくいっていないことの兆候だからということらしいです。
  • 複数単語を含む場合でも_-でつながず、some_packageの代わりにsomepackageを用いるのがよいとされます。
    • 前述通りpackageの名前がそれにアクセスするためのidentifierとなりますが、_が含まれるのは変数名の慣習と一致しません。
    • さらに大文字・小文字を区別しないファイルシステムが存在するためすべて小文字が好ましいという都合があります。
    • それらがあわさるとこういった慣習になっているのだと思います。

同じpackage内のファイルはnamespaceを共有しています: つまり別のファイルに同名の関数は定義できないし、別のファイルの関数や変数を利用可能です。

mkdir pkg1 pkg2
touch pkg1/some.go
touch pkg2/other.go

ファイルを以下のようにします

pkg1/some.go
package pkg1

var Foo = "foo"
pkg2/other.go
package pkg2

import (
  "fmt"

  "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1"
)

func SayDouble() string {
  return fmt.Sprintf("%q%q", pkg1.Foo, pkg1.Foo)
}

上記のように、ほかのpackageで定義した内容を利用するには、import宣言内で、fully qualifiedなpackageパスを書くことで、インポートします。

Go moduleは、循環インポートを許しません。つまり

pkg1/some.go
package pkg1

+import "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg2"

var Foo = "foo"

とすると以下のように import cycle not allowed エラーによりビルドできません。

$ go build ./pkg1
package github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1
        imports github.com/ngicks/go-example-basics-revisited/starting-projects/pkg2 from some.go
        imports github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1 from other.go: import cycle not allowed

外部のGo moduleをimportする(go get)

以下のコマンドドキュメントを参考にすると

https://pkg.go.dev/cmd/go

go get <<fully-qualified-package-path>>

で、Go moduleを取得し、go.modgo.sumを編集します。

例えば

$ go get github.com/ngicks/go-iterator-helper
go: added github.com/ngicks/go-iterator-helper v0.0.18

を実行すると以下のようにgo.modgo.sumにmodule情報が追記されます。

go.mod
module github.com/ngicks/go-example-basics-revisited/starting-projects

go 1.24.0

+ require github.com/ngicks/go-iterator-helper v0.0.18 // indirect
go.sum
+github.com/ngicks/go-iterator-helper v0.0.18 h1:a9a3ndHDyYSsI9bLTV4LOUA9cg6NpwPyfL20t4HoLVw=
+github.com/ngicks/go-iterator-helper v0.0.18/go.mod h1:g++KxWVGEkOnIhXVvpNNOdn7ON57aOpfu80ccBvPVHI=

まだこのmoduleはこのプロジェクトのどこからも使われていないので// indirectがつけれています。

importで各ソースコードにmoduleを導入して使用できるようになります。
goplsの設定でcompleteUnimportedが有効にされていると、可能な場合importに書かれていない内容でも補完がかかります(e.g. import clauseがないファイルでfmt.と打つとfmt.Printlnなどがサジェストされ、選ぶとimport "fmt"が追記される)ので、見た目に反してこの内容を書くのは面倒ではありません。(初めて導入したモジュールとかは補完されないことがある。)

cmd/example/main.go
package main

-import "fmt"
+import (
+  "fmt"
+
+  "github.com/ngicks/go-iterator-helper/hiter"
+  "github.com/ngicks/go-iterator-helper/hiter/mapper"
+  "github.com/ngicks/go-iterator-helper/hiter/mathiter"
+)

func main() {
  fmt.Println("Hello world", Foo)
+  fmt.Println(hiter.Sum(mapper.Sprintf("%x", hiter.Limit(8, mathiter.Rng(256)))))
}
$ go run ./cmd/example/
Hello world foo
9585b4cf88cdbe27

この時点でgo mod tidyを実行します。

go mod tidy
go.mod
module github.com/ngicks/go-example-basics-revisited/starting-projects

go 1.24.0

-require github.com/ngicks/go-iterator-helper v0.0.18 // indirect
+require github.com/ngicks/go-iterator-helper v0.0.18
go.sum
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/ngicks/go-iterator-helper v0.0.18 h1:a9a3ndHDyYSsI9bLTV4LOUA9cg6NpwPyfL20t4HoLVw=
github.com/ngicks/go-iterator-helper v0.0.18/go.mod h1:g++KxWVGEkOnIhXVvpNNOdn7ON57aOpfu80ccBvPVHI=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

実際にプロジェクト内で使われるようになったので// indirectが外れます。
さらにgo get時には追加されていなかった依存先の依存先もgo.sumに記録されています。

go mod tidyを行うとgo.sumの内容が整理されたり、使われなくなった外部moduleが削除されたりします。VCSにプッシュする前にはgo mod tidyを実行しておくほうがよいでしょう。

上記の例ではgo get時にversionを指定していないため、適当な最新バージョンが選ばられるようです。

下記のドキュメントにある通り、versinon queryによってversionの指定を行うことができます。

https://go.dev/ref/mod#version-queries

大体の場合下記のいずれかの方法で行うことになると思います

go get <<fully-qualified-module-path>>@latest
go get <<fully-qualified-module-path>>@v1.2.3
go get <<fully-qualified-module-path>>@<<git-tag>>
go get <<fully-qualified-module-path>>@<<git-commit-hash-prefix>>

git tagvでprefixされたSemantic Versioning 2.0形式であればgo.modにそのバージョンで記載されます(参照)。sem ver形式でなくてもよいですが、その場合はpseudo-versionというpre-release形式のversionにエンコードされて記載されます。
このpseudo-versionを直接指定しても取得できますが、指定したいrevisionの直前のsem verから1つ進んだversionのpre-releaseになる変換方式のようですので、これを直接指定するのは手間です。なのでやらないほうが良いでしょう。

internal: 外部公開しないpackage

internalという名前のdirectoryを作成すると、それ以下で定義されるpackageは、internal/と同階層かそれ以下からしかimportできなくなります。

例として以下のようにinternalを追加してみます。

.
├── cmd
│   └── example
│       ├── main.go
│       └── other.go
├── go.mod
├── go.sum
├── internal
│   └── i0
│       └── i0.go
├── pkg1
│   ├── internal
│   │   └── i1
│   │       └── i1.go
│   └── some.go
└── pkg2
    ├── internal
    │   └── i2
    │       └── i2.go
    └── other.go

内容は何でもいいのでとりあえず以下のようにします。

internal/i0/i0.go
package i0

const Yay0 = "yay0"
pkg1/internal/i1/i1.go
package i1

const Yay1 = "yay1"
pkg2/internal/i2/i2.go
package i2

const Yay2 = "yay2"

前述のとおり、internal/と同階層か、それより下からは参照できます。

pkg1/some.go
package pkg1

+import (
+  "github.com/ngicks/go-example-basics-revisited/starting-projects/internal/i0"
+  "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1/internal/i1"
+)

var Foo = "foo"
+
+func SayYay0() string {
+  return i0.Yay0
+}
+
+func SayYay1() string {
+  return i1.Yay1
+}
pkg2/other.go
package pkg2

import (
  "fmt"

+  "github.com/ngicks/go-example-basics-revisited/starting-projects/internal/i0"
  "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1"
+  "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg2/internal/i2"
)

func SayDouble() string {
  return fmt.Sprintf("%q%q", pkg1.Foo, pkg1.Foo)
}

+func SayYay0() string {
+  return i0.Yay0
+}
+
+func SayYay2() string {
+  return i2.Yay2
+}

もちろんこれらはビルドが成功します。

$ go build ./pkg1
$ go build ./pkg2

試しに下ではない階層からimportしてみます

pkg1/other.go
package pkg1

import (
  "github.com/ngicks/go-example-basics-revisited/starting-projects/internal/i0"
  "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1/internal/i1"
+  "github.com/ngicks/go-example-basics-revisited/starting-projects/pkg2/internal/i2"
)

var Foo = "foo"

func SayYay0() string {
  return i0.Yay0
}

func SayYay1() string {
  return i1.Yay1
}

+func SayYay2() string {
+  return i2.Yay2
+}

これは述べた通りエラーとなります。

$ go build ./pkg1
package github.com/ngicks/go-example-basics-revisited/starting-projects/pkg1
        pkg1/some.go:6:2: use of internal package github.com/ngicks/go-example-basics-revisited/starting-projects/pkg2/internal/i2 not allowed

package構成

https://go.dev/doc/modules/layout

  • 実行ファイルを作成するのが主眼となるmoduleはトップディレクトリがmainになることが多い
  • ライブラリとしてインポートすることもできるが、実行ファイルも提供する場合は以下のような構成になることが多い

実際にはプロジェクトの規模や意図などによってどのようにコードをオーガナイズするとよりよくなるかは変わるので、
個別の議論は避け、ほかの記事に譲るものとします。

.
├── cmd
│ ├── command1
│ │   ├── main.go
│ │   └── other_files.go
│ └── command2
│     ├── main.go
│     └── other_files.go
├── package_dir (名前はふさわしいものにする)
│   └── package.go
├── go.mod
├── go.sum
└── lib.go(名前はmoduleにふさわしい何かにする)

formatterの設定(goplsに任せるので特に設定はない)

goplsの設定でgofmt, goimports, gofumptなどでformatがかけられます。多分前述したeditorのセットアップをしたうえで、editorでFormat On Saveを有効にすれば問題なくformatがかかりますので特に設定はありません。

といいつつ、vim/neovimではさらに追加の設定が必要です。どうも試してる限りまだこのautocmdがないとimportの修正が起こらないっぽい?詳しくなくて裏が取れてません。
筆者はlua_lsに警告を受けるのが気に入らなかったので若干修正して使っています

linterの設定

goplsの設定でstaticcheckや、golang.org/x/tools/gopls/internal/analysis/modernizeなどが有効になっているはずです。

それ以外のいろいろなルールを追加したい場合は、github.com/golangci/golangci-lintがよく用いられると思います。

導入方法は下記で述べられていますが、vscode,GoLandに関してはextensionを入れる以外には特に設定がいらず、vim/neovimに関してはgolangci-lint-langserverを用いるように書かれています。
エディタのセットアップのところで触れたような感じで設定すればよいです(筆者の設定nvim-lspconfigの設定とマージされる前提です。)

https://golangci-lint.run/welcome/integrations/

ルールを自作し、golangci-lintから実行させたい場合は

などします。

どちらも始めたての人がいきなりできるものではないと思う(Goのastと型システムに対する習熟がいる。これは他言語の経験では補いづらい)ため、基本はgolangci-lintにあらかじめ統合されたもののみを使うとよいでしょう。

Private repositoryからgo getする

https://go.dev/ref/mod#private-modules

上記の説明より、一般公開されない、つまり特別な認証が必要なVCSでソースを管理し、go getなどでmoduleをインポート/ダウンロードする場合、

  • GOPROXYもしくはGOPRIVATEの設定
  • (web access時にauthが必要な場合)GOAUTHの設定
  • (GOPROXYを用いず、サブグループ下でソースを管理する場合)module nameを.gitでsuffixする
  • (GOPROXYを用いない場合)git credentialの適切な保存

を行う必要があります。

GOPROXYは筆者は試したことがないため、省略します。
VCSのcredentialはgitのみを想定します。

GOPRIVATE

GOPRIVATEの設定は以下で行います。

# git repositoryのURIが https://example.com/base_path
# である場合、<<url_wo_protocol>>は`example.com/base_path`になります。
go env -w GOPRIVATE=<<url_wo_protocol>>

(環境変数で指定すればよいと書かれていますが、筆者はうまくいかなかったのでgo env -wで書き込んでいます。)

GONOPROXY, GONOSUMDB(NOであることに注意)を設定しない場合、GOPRIVATEがデフォルトとして使われます。
GONOPROXYに設定されたホストからのmodule取得する(direct mode)際には相手VCSに合わせたコマンドが使用されます(gitの場合gitコマンド -> modfetch)。そのため、credentialの設定も多くの場合必要になります。

go env -wで書き込まれた内容はgo env GOENVで表示されるファイルに保存されます。

$ go env -w GOPRIVATE=example.com
$ cat $(go env GOENV)
GOPRIVATE=example.com
# -uでunset
$ go env -u GOPRIVATE
$ cat $(go env GOENV)

Dockerのbuild contextに渡したい場合などはこのファイルをマウントするとよいでしょう。

GOAUTH(筆者は試したことがない)

Go 1.23かそれ以前では、go toolがhttp accessを行う際にはcredentialを.netrcから読み込んでいましたが、Go 1.24からはGOAUTHを設定することで任意の方法を設定できるようになりました(デフォルトは.netrc)。

private VCSかつサブグループを使用する場合.gitなどでmodule nameをsuffixしておくのところで述べましたが、Go 1.23まで.netrc以外にcredを渡す方法がなかったためgitlabでは?go-get=1がついている場合credentialなしのHTTP GETを受け付けるようになっていました。そのため設定する必要があるのはこれ以外にもっときつい制限をかけたVCSで使用しているか、もしくはprivate go module proxyを用いる場合でしょうか。

筆者は試したことがないため参考までに、ですが、基本的にはgit dirを使用するとよいのではないかと思います。これはGOAUTH=git /path/to/working/dirを渡すと、dirでgit credential fillを呼び出しBasic AuthとしてHTTP Headerにセットするものなので、git credentialの設定がしっかりされていれば追加の設定が不要であるためです。

Basic Auth以外の認証方法が必要な場合はカスタムコマンドを渡します。go help goauthを参照してください。

(試してみたかったんですが、githubでprivate repositoryをdirect modeで取得する際にはGOAUTHを設定していなくてもよいみたいなので試せませんでした。)

module nameを.gitでsuffixする

👉private VCSかつサブグループを使用する場合.gitなどでmodule nameをsuffixしておく

git credentialの適切な保存

GOPROXYを用いない場合、VCSに対して直接VCSのコマンド(gitならgit ls-remoteなど)が実行されます。
これはdirect modeとドキュメント上呼ばれています。

この場合、gitならgitのcredentialを適切に保存しておく必要があります。private repositoryを利用している方はすでに設定しているかもしれないのでここは読み飛ばしてもらったほうがいいのかもしれません。

Git Credential Manager

git credentialの適切な保存には筆者はGit Credential Managerを利用しています。

  • windowsの場合、Git for windowsに付属してきますので、インストールオプションで一緒に入れます。
  • linuxの場合,Install instructionsに従いセットアップを行います。

vscodeの各種Remote extensionがgit credentialのヘルパーをつないでホスト環境につなげてくれるような挙動をしますので、
wslなどの場合はそちらを利用すればwincredにcredentialの保存が簡単にできます。

.netrc(非推奨)

.netrcはネットワークの認証情報を平文で保存しておくファイルらしく、linuxのman pageを検索するといくつかのコマンドがそれらを尊重するのがわかります。
フォーマットはIBMの「.netrc ファイルの作成」を参考にしてください。
${HOME}/.netrcあるいは${NETRC}にあるのが想定されるので適切に配置してください。

gitを含めた種々のコマンドが.netrcを読み込むようですが、GOAUTHが設定されていない場合はgo toolもこれを読み込む挙動となっています。

平文(=暗号化されていない)でcredentialを保存するフォーマットを推奨するべきではないはずなので、非推奨と述べておきます。

(おまけ)gpg-agentの設定

上記でGit Credential Managerを設定し、passを利用する設定にしている場合などでローカルのGnuPGが利用されるようになっており、環境がguiを表示可能な時

  • そもそもdesktopのGUI付きlinux
  • wslg
  • ssh configX11Forwarding yesにしている、など

にはgpg-agentをGUI付きのものにすると例えばtmuxスクリプトなんかでgpg-agentが呼び出されたときに気づかずパスワードの入力画面で固まらないため便利です。

# 筆者はみための好みでpinentry-qtを選択
sudo apt install pinentry-qt
$ cat ~/.gnupg/gpg-agent.conf
pinentry-program /usr/bin/pinentry-qt

ターミナルで入力を求めてくる形式のものはlazygitの画面ステートをぶっ壊したりして大変な目にあうかもしれないです(n敗)。

git-lfsを導入している場合はすべての環境でgit-lfsを使うように気を付ける

git-lfsというよりは導入有無でfetch結果のファイルコンテンツが変わってしまうプラグイン全般なのですが。

上記のような設定でdirect modeでGo moduleが取得される場合、
git-lfsの導入有無でgitからのfetch後の内容が異なることがあります。
これによってsum照合エラーでgo mod downloadが失敗する現象を何度か体験しています。

基本的にはすべての環境(Dockerfileなども含む)でgit-lfsを導入しておくほうがよいでしょう。

git-lfsGit Large File Storageのことで、gitで大きなファイルを取り扱うための拡張機能です。
git-lfsはhookとfilterを活用してコミット前後でトラック対象のファイルをテキストファイルのポインターに変換し、
トラックされた大きなファイルはremote repositoryではなく大容量ファイル用のサーバーに上げるような挙動になります。(参考: https://github.com/git-lfs/git-lfs, Git LFS をちょっと詳しく)

github, gitlab双方ともgit-lfsに対応しています。

開発の経緯的に、想定された用途ははゲームなどで大きなバイナリファイルを一緒に管理することのようです。
それ以外でもテスト用の大きなファイルを管理するときなどにも使うことがあると思います。

task runner

Goには組み込まれたtask runnerはありません。使わないか、外部のtask runnerを使います。

使わない

以下のケースではtask runnerを用いなくても十分通用します。

  • 複雑なビルド過程やテストマッチャーがない
  • github actions,gitlab ci, Dockerfileなどでビルドを記述できる
  • //go:generateで事足りる
  • .sh.batを両対応できる程度の量しかスクリプトがいらない
  • 管理用コマンドも全部Goで実装するつもり

Generate Go files by processing sourceより、//go:generate commandというマジックコメントを.goファイルの中に書き込むと、go generate ./path/to/file_or_packagecommandを実行できます。主眼はcode generatorを実行することですが実際には任意のコマンドを実行可能です。

依存moduleの管理はgo.modでできますし、moduleをvendorする機能もサポートされていますので、task runnerが必要ないケースも多いのではないでしょうか。

Make

makeを使っているプロジェクトをいくつか見たことがあります。
筆者はこの方法をとったことがないため何とも言えませんが、

  • pros:
    • linuxならもとからインストール済みのことが多い
  • cons:
    • makeはtask runnerではないという批判
    • syntaxの覚え方が苦しんで覚える以外の方法があるのかわからない
    • windowsで動かそうとするとハマる

という感じでしょうか(筆者の完全主観ですが)

チームがすでにmakeに慣れている場合はよい思います。

github.com/go-task/task

github.com/go-task/taskを用いるという方法もあります。

https://taskfile.dev/

Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.

とある通り、makeなどの代替を目指すものです。

  • pros:
    • Goで開発されているため、toolchainが入っていれば簡単にインストール可能
    • cross-platform
    • yamlで書ける
  • cons:
    • 管理すべきツールが増える
go install github.com/go-task/task/v3/cmd/task@latest

--initで初期化を行います。

$ task --init
Taskfile created: Taskfile.yml

デフォルトで以下が作成されます。

Taskfile.yml
# https://taskfile.dev

version: '3'

vars:
  GREETING: Hello, World!

tasks:
  default:
    cmds:
      - echo "{{.GREETING}}"
    silent: true

Templating Referenceにあるようにtext/templateの構文でtemplateを書けるようですね

Taskfile.yml
# https://taskfile.dev

version: '3'

vars:
  GREETING: Hello, World!

tasks:
  default:
    cmds:
-      - echo "{{.GREETING}}"
+      - echo "{{index .GREETING 0}}"
    silent: true
$ task default
72

72はHのascii codeです。

Usage#task-dependenciesより、task間に依存関係を記述可能です

Taskfile.yml
# https://taskfile.dev

version: '3'

vars:
  GREETING: Hello, World!

tasks:
  default:
    cmds:
      - echo "{{index .GREETING 0}}"
    silent: true
+  quack:
+    cmds:
+      - echo quack
+  run:
+    deps: [quack]
+    cmds:
+      - go run ./cmd/example
$ task run
task: [quack] echo quack
quack
task: [run] go run ./cmd/example
Hello world foo
ede693e1e85a2f70

最近のpowershell(というかwindowsに?)にはunix風コマンドがいくつかあります(tarとかcurlとか)。echoは存在するみたいなのでこれはwindowsでも動作するようです。

deno task

denoのtask runnerを用いる方法もあると思います

https://docs.deno.com/runtime/reference/cli/task/

  • pros:
    • cross-platform
    • jsonで書ける
    • daxを用いた容易なスクリプト開発
  • cons:
    • 別言語の知識が必要
    • 管理すべきツールが増える

denoRusttokioをバックエンドに、javascript engineのV8で動作するjavascript/typescriptランタイムです。

daxを用いるとshellscriptのようなノリでtypescriptがかけるためいい感じです。
なんとshellコマンド間やjavascript objectにpipeが行えるのです。shellscriptで書くには億劫な高度な演算をtypescriptでかいたり、コマンドの結果をWebStreamに受けていじくったりできるので便利だと思います。

ただ半面導入するツールが増えのが難点です。Goで開発されているわけでもないためぱっと導入できるわけでもないですし、書き込まれるキャッシュ領域も増えるので、必ずしもこれが最適な選択というわけでもありません。
チームがすでにtypescriptに慣れており、何かの事情ですでにdenoを導入している場合はよいかもしれません。

taskであげた例と似たようなものは以下のようになります。

dneo.json
{
  "tasks": {
    "quack": "deno eval 'console.log(\"quack\")'",
    "run": {
      "command": "go run ./cmd/example",
      "dependencies": ["quack"]
    }
  },
  "imports": {
    "#/": "./script/"
  }
}
$ deno task run
Task quack deno eval 'console.log("quack")'
quack
Task run go run ./cmd/example
Hello world foo
ed35803c9ff5b841

daxを用いるとshellのようにtypescriptがかけます。
少し極端な例としてsha256sumをとるのをshellだけでやるような形と、WebStreamを混在させたバージョンの二つを挙げてスタイルの自由さの例とします。
builtInCommandsdaxが抽象化しているのでwindowsでも動きます。sha256sumawkはないので実際にはWebStream版のようなことをすることになるでしょう。

script/dax_example.ts
import { crypto } from "jsr:@std/crypto";
import { encodeHex } from "jsr:@std/encoding/hex";

import $ from "@david/dax";

const sum1 = await $`cat ./go.mod | sha256sum | awk '{print $1}'`.text();

const pipe = new TransformStream<Uint8Array, Uint8Array>();
const sumP = crypto.subtle.digest("SHA-256", pipe.readable);
await $`cat ./go.mod > ${pipe.writable}`;
const sum2 = encodeHex(await sumP);

console.log(sum1);
console.log(sum2);
console.log("same? =", sum1 === sum2);
$ deno run -A ./script/dax_example.ts
a141062eb619fb89a183d62b8896d192170b1dd6fc479611f6c5a427038447f0
a141062eb619fb89a183d62b8896d192170b1dd6fc479611f6c5a427038447f0
same? = true

dax・・・すばらしい・・・

おわりに

private repositoryからgo getするのは特に躓いたのでまとめておきました。

GitHubで編集を提案

Discussion