Goのプラクティスまとめ: プロジェクトを始めるまで
Goのプラクティスまとめ: プロジェクトを始めるまで
筆者がGoを使い始めた時に分からなくて困ったこととか最初から知りたかったようなことを色々まとめる一連の記事です。
以前書いた記事のrevisited版です。話の粒度を細かくしてあとから記事を差し込みやすくします。
他の記事へのリンク集
- (まだ)
今はこうやる集 - プロジェクトを始めるまで: ここ
- dockerによるビルド
- error handling
- (まだ)
fileとio - (まだ)
jsonやxmlを読み書きする - (まだ)
cli - (まだ)
environment variable - (まだ)
concurrent Go - (まだ)
context.Context: long running taskとcancellation - (まだ)
http client / server - (まだ)
structured logging - (まだ)
test - (まだ)
filesystem abstraction
EDIT(2025-11-18)
大幅改定(change log)
プロジェクトを始めるまで
新しいプログラミング言語、フレームワーク、ライブラリー、ツール、etcを始めるとき筆者にとってよく障害となるのは「プロジェクトを始めるまでの方法がわからない」ということです。
そこで、この記事では
-
Goそのものの紹介
- A Tour of Goの紹介や、読み物系へのリンク
- SDK(Software Developement Kit = コンパイラとかライブラリのことをさす)のインストール
- エディターのセットアップ
- vscodeなど
- プロジェクトの開始方法(=moduleの作成方法)
- github / gitlabなどにrepositoryを作成してgo moduleを作って動作させるまで
- private repositoryで管理されるgo moduleをインポートできるようにする方法
- おまけとしてtask runnerについて
などについてまとめます。
対象読者/前提知識
環境
win11のwsl2インスタンス内で動作させます。
windowsでの手順も紹介しますが普段使う環境は下記なのでほかの環境への考慮は甘いかもしれません。
$ wsl.exe --version
WSL バージョン: 2.6.1.0
カーネル バージョン: 6.6.87.2-1
WSLg バージョン: 1.0.66
MSRDC バージョン: 1.2.6353
Direct3D バージョン: 1.611.1-81528511
DXCore バージョン: 10.0.26100.1-240331-1435.ge-release
Windows バージョン: 10.0.26100.6584
Goは現在(2025-10-11)の最新です。
$ go version
go version go1.25.2 linux/amd64
Goは後方互換性をかなり気にするためサンプルコードや述べられていることは以降のバージョンでも(何ならこれよりの前のバージョンでも)基本的に一緒ですが、細かいところで改善が入ったりするのでそれを踏まえて読んでください。
一方で、多くのライブラリ直近の2 major versionのみサポートするライブラリが多いとして、Go 1.24以降で新規にできるようになったことは、なるだけ○○以降と書くようにします。
snippetのconvention
- shellコマンドの羅列の場合コピペしやすさを優先して特になにもprefixをつけずにコマンドを羅列します。
- ただし、コマンド実行結果をsnipet内の併記したい場合は、コマンドは
$でprefixされます。 -
#から始まる行はコメントです。
筆者のバックグラウンド
どういう立場からいてるのかっていうのあると多分いいと思たので載せときます。
学生時代にC++(Visual Studio Community 2018だったと思う)を使ってセンサーから値を読み込んで計算を行うプログラムを作っていました。機械系の学部だったので装置含めて広く浅くだったのでした(あんまりまじめな学生じゃなかったのですしね)。そのため当時からして古い記法を使っていたと思います。何ならUIにMFCを使っていました(いまなら厳密なリアルタイム性を要求するところ以外は各プラットフォームのwebviewを使うことになるでしょう)。 実験室PCと居室PCのwindowsバージョンが合わなくてビルドが通らないとか頻発してました。
社会人になってからNode.js(TypeScript)でいろいろAPIサーバーを実装, pythonをちょっとだけ使う。RustでPDFiumとかOpenSSLのbindingを書いて利用していました。
GoはgoroutineというGreen Thread(osが提供するthreadと違って、ランタイムが独自にコントロールするがthreadのように扱えるもの。)を持っていることと、簡素な言語仕様で有名でした。
goroutineがあればasync/awaitの良くある問題である、
-
asyncな関数を導入するとそれの呼び出し側も基本的にはすべてasyncにしなければならない -
asyncな関数に長い時間ブロックする関数を導入すると破綻する
という問題が発生しないため注目し、使い始めました。
少し特殊な環境で動くソフトウェアを書くためデータベース周りの話にあまり明るくなかったりします。
手元でスクリプティングを行う場合はもっぱらneovimのLua、deno, Goのいずれかで行っています。
基本: Goとは
Goとはどういう言語なのかと基本的な読み物系の紹介。
特徴
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系統の文法で
- 静的型付けで
-
interfaceによるdynamic dispatch(compile時ではなくruntimeにおいて呼び出される関数が決定される仕組み)があり - GCがあり
- (GC=garbage collector/collection=いらなくなったメモリを自動的に開放する仕組み。allocation/freeのメモリ管理を手動でやる必要がないという意味)
- 文法や構文が厳選されており、追加もめったにないため書き方がブレにくく
- 言語に組み込まれた
goroutine, いわゆるGreen Threadの機能があります。- そのため
async/await的な記法がない -
goroutineはgoキーワードの後に関数の呼び出しを書くだけでよく、簡単。 - 言語に組み込まれているので非同期性の実装のためにライブラリが分断されることない。
- そのため
- コンパイルが非常に速く(遅くない/遅くならないというほうが正しいがここでは置いておく)
- (C-bindingを使わない限り)クロスコンパイルが簡単で
- (クロスコンパイル/クロスビルド=linuxでwindows向けのビルドを行ったり、その逆を行ったりするような感じで、ある環境から別の環境向けの実行ファイルを出力すること)
- (C-bindingを使わない限り)staticなシングルバイナリを簡単に出力することができ
- (static=プログラム実行時に動的ロードされるライブラリがない、つまりos/architectureが同じならどこでも動く)
- 組み込まれたモジュールとパッケージマネージャの仕組みがあり
みたいな言語です。
A Tour of Go
公式から提供されるチュートリアル。
インタラクティブなコードスニペット/実行環境と、簡単な課題があり、これさえこなせばとりあえず開発は始められます。
筆者の記憶にある限り筆者は合計6時間ぐらいですべて終わりました(当時はGo1.16あたりだったので今はもう少し長い)。
時間のほとんどは構文エラーがよくわかんなくて費やされたのでローカルのエディターにコピーして書いてコピーしなおして実行すればもっと早く終わるかもしれません。
ということで一旦ここは飛ばしてもらってエディターのセットアップを先にしてもらったほうがいいかもしれないですね。
Go by Example
コード例とともに解説がされます。
項目数が多く、知らないstd moduleを使う部分をきちんと理解しようとすると時間がかかると思います。
手が空いたら少しずつ読むのがいいのではないでしょうか。
公式読み物系
公式が出している読み物集。全部英語です。
内容が古くなりつつあるため、始めたてで読むべきかは微妙ですが、古くなっても参考になる部分が大いにあります。
- How to Write Go Code: https://go.dev/doc/code
- コードの書き方以外も含めた基本的なトピック
- Effective Go: https://go.dev/doc/effective_go
- Goのイディオム集
- 現在(2025-10-12)だいぶ古くなってきているので、一通り読んだらほかのドキュメントも読むようにしてください
- Go Wiki: Go Code Review Comments: https://go.dev/wiki/CodeReviewComments
- よくされるCode review comment集らしいです
そのほかにもいろいろなトピックが以下に掲載されています。
The Go Programming Language Specification
言語仕様ですが割と短めなのでそのうち読んでおいたほうがよいでしょう。
Std library
standard libraryです。
HTTPなどで動作するサーバープログラムを作るのに大体必要な機能がそろっています。
できれば開発に着手する前にさっくりどういう機能がstdとして存在するかを把握しておくほうがよいでしょう。
CGOとSWIGへの言及があるなど、読んどけばよかった系の文章が意外なほどたくさん書いてあります。
Sub-repositories
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
公式でメンテされているexample集。
部分的に古かったりする割に、go/types周りの話など結構アップデートされている部分もある。
プロジェクトの始め方
go moduleを作ってプログラミングを介するまでのあれこれをまとめておきます。
Goの文法周りの話はA Tour of Goで網羅されているので説明しません。
これ以降はA Tour of Goをこなしことを前提とします。
Goをインストールしてエディタをセットアップし、VCS(Version Control System)にrepositoryを一つ作り、そこに1つgo moduleを作るところまでをここでカバーします。
VCSはここではgitしか想定されていません。
Goのインストール
公式の手順に従い、各OS環境に合わせてGoをインストールしましょう。
Windows
説明のとおり最新バージョンの.msi形式のインストーラーを実行しましょう。
インストール先のフォルダを選ぶ以外に選択する個所はありません。
$ go version
go version go1.25.2 windows/amd64
一応PATHに$(go env GOPATH)/bin以下が含まれるか確認しておきましょう。
ない場合は、かつてインストールして環境変数を手動で消したときかと思います。
$ go env GOPATH
C:\Users\ngicks\go
$ $env:PATH
C:\WINDOWS\system32;..(省略)..;C:\Users\ngicks\go\bin
Linux
ダウンロード
linux/amd64の場合、
VER=1.25.2
cd $(mktemp -d)
curl -L https://go.dev/dl/go${VER}.linux-amd64.tar.gz -o ./dist.tar.gz
一応チェックサムを確認しておいたほうがいいかもしれません。
# checksumの値はダウンロードページから確認できる。
CHECKSUM=d7fa7f8fbd16263aa2501d681b11f972a5fd8e811f7b10cb9b26d031a3d7454b
ACTUAL=$(sha256sum ./dist.tar.gz | awk '{print $1}')
[[ $CHECKSUM = $ACTUAL ]]; echo $?
一致してれば0がprintされます。
インストール
公式は/usr/local以下に入れる方法を案内しています。
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xf ./dist.tar.gz
PATHが通っていればどこにおいても大丈夫です。
共有サーバーなどで使用する場合は$HOME以下に解凍するほうが無難かもしれないです。
PATHを通す
環境変数を設定。使っているOS/terminalに合わせた方法で設定してください。
例えば、以下のようなスクリプトを組んで.bashrcなどから. /path/to/scriptという風に呼び出すとよいでしょう。
gobin=/usr/local/go/bin/go
gorootbin=($gobin env GOROOT)/bin
case ":${PATH}:" in
*:"$gorootbin":*)
;;
*)
export PATH="$gorootbin:$PATH"
;;
esac
ほっとくと~/goにgo moduleのキャッシュとかが入ってしまいます。
$HOME以下にディレクトリが増えてほしくないなら以下のように適当なパスに設定しておきます。
# I'm not using ~/.cache/go since it is somehow populated
export GOPATH="${XDG_DATA_HOME:-$HOME/.local/share}/go"
export GOBIN="${XDG_DATA_HOME:-$HOME/.local/share}/go/bin"
case ":${PATH}:" in
*:"$GOBIN":*)
;;
*)
export PATH="$GOBIN:$PATH"
;;
esac
Go Modules Reference#go-installより環境変数もしくはgo envでGOBINが設定されていた場合、go installでコンパイルされた実行ファイルは設定されたディレクトリに格納されます。設定されない場合、${GOPATH}/bin以下に格納されます。
例としてlazygitをgo installしてみましょう。
go install github.com/jesseduffield/lazygit@latest
which lazygit
# 筆者の環境では`$(go env GOPATH)/bin/lazygit`が表示されます。
mise(windows/linux/mac)
mise-en-place(以後単にmiseと呼ぶ)を使う方法。
もしかしたら一番おすすめかも。windowsでも動作すると書かれていますが筆者はwindowsでは試したことはないです。
miseはsdk・環境変数のマネジメント、タスクランナーを全部できるソフトウェアです。
サポートされるsdkはbun,deno,elixir,erlang,Go,Java,Node.js,Python,Ruby,Rust,Swift,Zigのみですが(2025-10-12時点)、そもそもgithub/gitlabのrelease pageからツールを落としてきて解凍して配置する機能があるため、大抵何でも管理できます。
npxやgo install,cargo installによってインストールできるツールの管理も行えるのがお勧めなポイントです。
miseのインストール
公式の案内通りに行います。
self-update機能があるため、最新を常に使うみたいな用途だと一番上の方法がいいかもしれません。
curl https://mise.run | sh
もちろんshellに渡す前に落とされるスクリプトはよく読みましょう。
miseの設定
などを参考に、以下のような感じで
touch ~/.config/mise/config.toml
touch ~/.config/mise/config.lock
[settings]
experimental = true
auto_install = true
[tools]
go = "latest"
"go:golang.org/x/tools/gopls" = { version = "latest" }
"go:golang.org/x/tools/cmd/goimports" = { version = "latest" }
"go:mvdan.cc/gofumpt" = { version = "latest" }
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = { version = "latest" }
"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = { version = "latest" }
"go:github.com/go-delve/delve/cmd/dlv" = { version = "latest" }
mise trustで信頼できるconfigに指定しないと初回時にtrustするか聞かれます。
あらかじめmise trustしておいたほうがプロンプトが出ないので便利かもしれません。
mise trust ~/.config/mise/config.toml
mise activate, install, up
mise activateで前述の設定をglobalに有効にします。
例えば、以下のようなスクリプトを組んで.bashrcなどから. /path/to/scriptという風に呼び出すとよいでしょう。
# bashの場合
eval "$($HOME/.local/bin/mise activate bash)"
# zshの場合
eval "$($HOME/.local/bin/mise activate zsh)"
mise install
ですべてのツールがinstallできます。
$ which go
/home/ngicks/.local/share/mise/installs/go/1.25.2/bin/go
という感じで適当なパス以下に導入されます。miseを消すときは~/.local/share/miseを消したら全部おしまいなのでらくちんですね。
mise up
ですべてのツールの更新ができます。
最近はサプライチェーン攻撃が怖い([1])ので"1.25.2"のようなexact versionを指定してrenovateで自動更新してもらうほうがいいかもしれないですね。
参考までに: 筆者のdotfilesのmise config
エディタのセットアップ
エディタは個人の好みともろもろを合わせて好きに選べばいいと思います。
ただよく聞く+goのsurveyの上位3位は以下の3通りです。
- Visual Studio Code
- JetBrainsのGoLand
- vim / neovim
下記でvscodeとneovimの設定を紹介します。
GoLandは使ったことないのと多分入れたら終わりなので特に紹介在りません。
vim版も似たような設定になる気はしますが、筆者はneovimしか使っていないのでvim側の設定の紹介はありません。
goplsによる言語サーバー支援、format on save, lintの設定をします。
gopls/言語サーバー/LSPとは
lintとは
lintとは静的コード解析のことをさします。
静的、というのは要するにプログラムを構文的に解析するだけで、実行はしないことをさします。
go testなどで実行できるテストはコードがどのようにふるまうかを主として検査しますが、lintはコードの書き方などを含めたふるまい以外の部分を主として検査します。
linterは言語サーバーに組み込まれていたり、別のコマンドとして実装されていたりします。
javascriptならeslintやbiome、pythonならpylint、Goならstaticcheckなどがあります。
一つ一つの検査項目は(筆者の観測範囲では大抵)lint ruleというような呼び方をします。Goの場合はanalysisと呼ぶことがあります。lint ruleによってはautofixを提供するものもあります。
lint ruleには例えば下記のようなものがあります
-
nilness: 絶対にnilにならない値に対して
if a == nilのチェックをしている部分に警告 -
modernize: 追加された構文を使っていない部分に警告(e.g.
for i := 0; i < LIMIT; i++->for range LIMIT,sort.Sort->slices.Sort) - lll: 1行が長すぎると警告
ソースコードの書き方自体に対して警告し、ふるまいには(実行できないので)直接的に関心を持ちません。
Visual Studio Code
Go extensionを入れたら終わりです。
初めてGoのプロジェクトを開いたときに依存先をインストールするかのポップアップが出るので、インストールしておきます。
もしくはCtrl+Shift+P(環境によってバインドは異なるかも、特にmac)でcommand palletを開き、Go: Install/Update Toolsと打ち込んでEnterします。
もう少しカスタマイズしたいならgoplsの設定を行います。
vscodeを開き、Ctrl+Shift+Pでcommand palletを開き、Preferences: Open User Settings(JSON)と打ち込んでEnterします。
以下を追記します。(コメント行はコピーしないでください。)
{
"editor.formatOnSave": true,
// https://github.com/golang/tools/blob/master/gopls/doc/settings.md
"gopls": {
"ui.semanticTokens": true,
"ui.diagnostic.analyses": {
// https://staticcheck.dev/docs/checks
"nilness": true,
"nonewvars": true,
"useany": true
}
},
"go.lintTool": "golangci-lint-v2"
}
リンク先を参考に設定をいろいろ変えてみてください。
とりあえずsemanticTokensは現状デフォルトでfalseです: #45313
"go.lintTool": "golangci-lint-v2"はstaticcheckに不足ある場合のみ設定します。
neovim
v0.11のみの話します。versionごとに設定違うかもしれないので以降のバージョンだと違うかも。
$ nvim --version
NVIM v0.11.5
Build type: Release
LuaJIT 2.1.1741730670
Run "nvim -V1 -v" for more info
依存先のインストール
まず依存ツールを収集します。
下記のリストを参考にgo installを個別にinstallするか、
もちろんmiseで管理してもよいと思います。
[tools]
go = "latest"
# gopls本体
"go:golang.org/x/tools/gopls" = { version = "latest" }
# formatter
"go:golang.org/x/tools/cmd/goimports" = { version = "latest" }
"go:mvdan.cc/gofumpt" = { version = "latest" }
# debugger
"go:github.com/go-delve/delve/cmd/dlv" = { version = "latest" }
# test generator
"go:github.com/cweill/gotests/gotests" = { version = "latest" }
# goplay client
"go:github.com/haya14busa/goplay/cmd/goplay" = { version = "latest" }
# linter
"go:honnef.co/go/tools/cmd/staticcheck" = { version = "latest" }
ただし依存先は変わることがあるのでallTools.ts.inの内容から生成したほうがいいかもしれないですね。
goplsの設定
- 適当なpackage managerでneovim/nvim-lspconfigを
rtpに加えておく。-
require "lspconfig"してエラーしなければいいということです。
-
-
neovim/nvim-lspconfigの設定では不足ある場合は、~/.config/nvim/after/lsp/gopls.luaを作成して適当に設定を上書きする。 - どこかの
luaスクリプトからvim.lsp.enable("gopls")を呼び出す。 -
goplsをインストールして$PATHを通す。-
williamboman/masonで管理したいなら
ensure_installedに"gopls"を指定しておくとよい。
-
williamboman/masonで管理したいなら
筆者は~/.config/nvim/after/lsp/gopls.luaを以下のようにしています。
-
autocmdを設定しないとフォーマットがかからなかったので入れています。 - 設定はverboseだと思ったらfalseにしています。
-
completeUnimported = trueがあるとまだimportしてない依存先のauto completeも動作するようになります(デフォルトはfalse)。あるとないでは体験がだいぶ違います。 - 詳しくないのでもっと簡易な設定があったらすみません。
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.go",
callback = function(args)
local clients = vim.lsp.get_clients { name = "gopls" }
if #clients == 0 then
return
end
local client = clients[1]
local pos_encoding = vim.lsp.get_client_by_id(client.id).offset_encoding or "utf-16"
local params = vim.lsp.util.make_range_params(vim.fn.bufwinid(args.buf), pos_encoding)
params = vim.tbl_deep_extend("force", params, { context = { only = { "source.organizeImports" } } })
-- buf_request_sync defaults to a 1000ms timeout. Depending on your
-- machine and codebase, you may want longer. Add an additional
-- argument after params if you find that you have to write the file
-- twice for changes to be saved.
-- E.g., vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 3000)
local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params)
for cid, res in pairs(result or {}) do
for _, r in pairs(res.result or {}) do
if r.edit then
local enc = (vim.lsp.get_client_by_id(cid) or {}).offset_encoding or "utf-16"
vim.lsp.util.apply_workspace_edit(r.edit, enc)
end
end
end
vim.lsp.buf.format { async = false }
end,
})
return {
settings = {
gopls = { -- https://github.com/golang/tools/blob/master/gopls/doc/settings.md
analyses = {
-- https://staticcheck.dev/docs/checks
ST1003 = false,
fieldalignment = false,
fillreturns = true,
nilness = true,
nonewvars = true,
shadow = false,
undeclaredname = true,
unreachable = true,
unusedparams = true,
unusedwrite = true,
useany = true,
},
codelenses = {
generate = true, -- show the `go generate` lens.
regenerate_cgo = true,
test = true,
tidy = true,
upgrade_dependency = true,
vendor = true,
},
hints = {
assignVariableTypes = true,
compositeLiteralFields = true,
compositeLiteralTypes = true,
constantValues = true,
functionTypeParameters = true,
parameterNames = true,
rangeVariableTypes = true,
},
buildFlags = { "-tags", "integration" },
completeUnimported = true,
diagnosticsDelay = "500ms",
gofumpt = true,
matcher = "Fuzzy",
semanticTokens = true,
staticcheck = true,
symbolMatcher = "fuzzy",
-- I've used this for a while, and found it annoying.
-- Great feature tho.
usePlaceholders = false,
},
},
}
golangci-lintの設定
golangci-lintはいろんなlintツールをまとめて実行できるようなもので、デファクトスタンダード化してるのでこの記事としては紙面を割かねばなりません。
golangci-lint自体はcliツールですが、golangci-lint-langserverというさらなる外部コマンドによって言語サーバーとして使用できます。
基本はnvim-lspconfigに設定があるのでmasonでgolangci-lint,golangci-lint-langserverをインストールし、configのどこかからvim.lsp.enable("golangci_lint_ls")を呼び出せばとりあえずは良いです。
下記です。
ただしこの設定ではgolangci-lintのv1,v2のconfig非互換問題を解決できていません。
golangci-lintにはv1, v2があるんですが、それらはconfigのフォーマットに互換性がありません。v2は比較的最近(2025-03-24)出たので、世間に存在する設定ファイルはv1, v2混在していています。
そこで、configに合わせてv1, v2を切り替えるようにします。
めっちゃmiseに依存してます。v1, v2両方入れておきます。
[tools]
"go:github.com/nametake/golangci-lint-langserver" = { version = "latest" }
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = { version = "latest" }
"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = { version = "latest" }
golangci-lint config verifyをv2, v1両方で実行し、passしたほうの設定を使います。
local markers = {
".golangci.yml",
".golangci.yaml",
".golangci.toml",
".golangci.json",
}
return {
cmd = { 'golangci-lint-langserver' },
filetypes = { 'go', 'gomod' },
root_dir = function(bufnr, on_dir)
local fname = vim.api.nvim_buf_get_name(bufnr)
local root = vim.fs.root(fname, markers)
if root then
on_dir(root)
end
end,
root_markers = markers,
before_init = function(_, config)
-- switch version based on config schema.
-- golangci-lint v2 is relatively new.
-- So some projects still are sticking to v1,
-- while some other has been migrated to v2.
-- check v2 first since basically it is stricter
-- in some aspect, e.g. required version top element.
local v2 = vim
.system({
"mise",
"exec",
"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint",
"--",
"golangci-lint",
"config",
"verify",
})
:wait()
if v2.code == 0 then
config.init_options.command = {
"mise",
"exec",
"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint",
"--",
"golangci-lint",
"run",
"--output.json.path=stdout",
"--show-stats=false",
}
return
end
local v1 = vim
.system({
"mise",
"exec",
"go:github.com/golangci/golangci-lint/cmd/golangci-lint",
"--",
"golangci-lint",
"config",
"verify",
})
:wait()
if v1.code == 0 then
config.init_options.command = {
"mise",
"exec",
"go:github.com/golangci/golangci-lint/cmd/golangci-lint",
"--",
"golangci-lint",
"run",
"--out-format",
"json",
}
return
end
vim.notify('"golangci-lint config verify" failed for both v1 and v2', vim.log.levels.WARN)
end,
}
プロジェクトを始める
プロジェクトを始めるということは、実行ファイルを作るか、ライブラリを作るということを意味します。
Goでプロジェクトを作成するには、どちらに対してもGo moduleを作成します。
プログラミング言語/ビルドシステムによってはディレクトリ構成が指定されていることがありますが、Goの標準的なビルドシステム(=go build)には特に指定はありません。
Goではmainという名前のpackageで実行ファイルのエントリーポイントが定義され、それ以外の名前のpackageがライブラリとしてimport可能です。
- VCS(version control system)上でrepositoryを作成してローカルにcloneします。
-
go mod initでGo moduleを初期化します。 -
main packageを定義して実行ファイルをビルドできることを示します。 -
packageを分割し、importしあう方法を示します。 - 外部の
Go moduleをfetchしてimportする方法を示します。 -
internal packageによる公開性のコントールについて示します。
VCS(git)でrepositoryを作成する
VCS(Version Control System)は, コンピュータファイルのバージョンを管理するシステムのことです。
代表的なものはgitやsvn,mercurialあたりだと思います。
この記事ではgitのみを取り扱います(筆者がほか二つのことをほぼまったく知らないからです)
gitは、VCSを構築するためのサーバーおよびクライアントプログラムです。サーバーとして直接使うことはほとんどないかもしれません。
現在ではgitサーバーはgithubというwebサービスを利用するか、 セルフホストすることも可能なgitlab、あるいはgitbucketなどを使うのが一般的だと思います。(この3つがリストされてるのは単に筆者が使ったことあるやつ3種っていうだけです)
最近ではGiteaも人気なようです。
ホスティングサービスでrepositoryを作成する
ソースコードを管理するためのrepositoryを作成しておきます。
手順そのものは説明しませんが、以後の手順はgithubやgitlabでrepositoryが存在していることを想定します。ローカルで作成してからremoteを指定しても構いません。
git clone
先ほどgit(VCS)で作成したrepositoryをローカルにgit cloneしておきます。
cloneで作成されたディレクトリにcdで移動しておきます。ここがGo moduleのmodule rootとなります。
git clone ${uri}
cd ${repo-name}
おすすめなのは${GIT_REPOSITORY_BASE}/${domain}/${path}のディレクトリ構成をとっておくことです。こうすれば同じ名前のrepositoryがあってもパスが被ることがなくなります。
例えば以下のような感じ
# uriが
# https://github.com/ngicks/go-example-basics-revisited
# であるとき
mkdir -p "${HOME}/gitrepo/github.com/ngicks/go-example-basics-revisited"
git clone https://github.com/ngicks/go-example-basics-revisited "${HOME}/gitrepo/github.com/ngicks/go-example-basics-revisited"
cd "${HOME}/gitrepo/github.com/ngicks/go-example-basics-revisited"
このスタイルでの管理をツール化する際には、筆者は使っていないですが以下のようなものを使うのもいいかもしれないです。
Go moduleの初期化
-
Goでプログラムを作成するにはGo 1.11以降、Go moduleを作成します。 -
Go moduleは1つないしは複数のpackageからなります。 -
go.modファイルが存在しているディレクトリ(フォルダ)とそより下のディレクトリの階層がgo moduleとなります。 - ほかの言語と同様に、コマンド(
go mod init)からGo moduleを初期化します。
go mod init
Go moduleを初期化します。
go mod init ${module_name}
${module_name}は基本的に先ほど作成したVCS repositoryの${uri}からプロトコルスキームを抜いたものにします。
先ほどの例で行くと、${uri}はhttps://github.com/ngicks/go-example-basics-revisitedであるので
go mod init github.com/ngicks/go-example-basics-revisited
となります。
go mod initによってgo.modが作成されます。
これを含めてVCSにプッシュすると
go get github.com/ngicks/go-example-basics-revisited
でほかのGo moduleから導入、参照できます。
local onlyのモジュール
(private gitかつサブグループを使用する場合)module nameに.gitをつける
一部のVCS, gitlabなどは通常のhttps://${domain}/${organization}/${reponame}階層構造を超えて、さらにサブグループを作成することができます。
つまりhttps://${domain}/${organization}/${group_name1}/.../${group_nameN}/${reponame}となるわけですね。
そのような2階層以上のグループを持ち、privateなgit出る場合、Go moduleの名前の末尾に.gitとつけなければならないことがあります。
より正確に言うと、git cloneしないといけないrepositoryのパスとなる部分に.gitとつける必要があります。
https://${domain}/${organization}/${group_name1}/.../${group_nameN}/${reponame}.git/path/to/submodule
gitlabを用いて行った検証
行った検証を記します。
publicだと両方go getできました。
現在(2025-10-13)gitlabで
- privateのgroup/subgroupを作り、その下にprojectを1つ作ります。
- projectをcloneし、サブディレクトリを二つ作ります。
- 片方で
go mod init gitlab.com/${group}/${subgroup}/prj/sub1とし - もう片方で
go mod init gitlab.com/${group}/${subgroup}/prj.git/sub2とします。 - プッシュしておきます。
cd $(mktemp -d)
go mod init sample.test
export GOPRIVATE=gitlab.com/${group}
$ go get gitlab.com/${group}/${subgroup}/prj/sub1
go: module gitlab.com/.../proj/sub1: git ls-remote -q origin in /home/ngicks/.local/go/pkg/mod/cache/vcs/be5b4b9be0c9e04a2fc93d37972ba2206efb91f39ed469c1442b1b1d
9ee34ab3: exit status 128:
remote: The project you were looking for could not be found or you don't have permission to view it.
fatal: repository 'https://gitlab.com/.../subgroup.git/' not found
$ go get gitlab.com/${group}/%{subgroup}/prj.git/sub2
go: downloading gitlab.com/...
go: downloading gitlab.com/...
go: added gitlab.com/...
どうしてなのかの詳しい話
Goでpublicではないregistryからmodule fetchを行いたい場合GOPRIVATE環境変数にuriのプロトコルスキーム抜きのものを指定します(大抵の場合ドメイン)。
GOPRIVATEが設定されているがGOPROXYが特に指定されていないとdirect accessと言って、gitなどのVCSに対応するコマンドを使って直接VCSからmoduleを取得しようとします。
go toolはmodule pathを与えられるとmodule pathに?go-get=1をつけてhttp getすることでmodule metadataを得ようとします。
$ curl 'https://gitlab.com/ngicks/subgroup-sample/prj/sub1?go-get=1'
<html>
<head>
<meta name="go-import" content="gitlab.com/ngicks/subgroup-sample git https://gitlab.com/ngicks/subgroup-sample.git">
</head>
<body>go get gitlab.com/ngicks/subgroup-sample</body>
</html>
返答は<meta name="go-import" content="root-path vcs repo-url [subdirectory]">のフォーマットが期待されます。
go toolはこの情報を使ってVCSから特定のバージョンのソースコードを取り出します。
公開repositoryであればソース情報はhttps://proxy.golang.orgにキャッシュされます。
問題はmodule pathがprivateな場合です。
例として以下のようにprivateグループ以下の存在しないパスに対して?go-get=1付きでリクエストしたとします。
$ curl 'https://gitlab.com/ngicks-group/aaaa/bbbb?go-get=1'
<html><head><meta name="go-import" content="gitlab.com/ngicks-group/aaaa git https://gitlab.com/ngicks-group/aaaa.git"></head><body>
このように、存在しないパスに対してリクエストを行った場合、オウム返しのように要求されたパスが埋められた"go-import"が返って来ます。
Go 1.24まで、.netrcを用いる以外にgo toolにcredentialを渡す方法がありませんでした。.netrcは平文で機密情報を書き出すフォーマットであるため普通のユーザーの環境では準備されていることはあまりありません。そのため?go-get=1は認証情報なしでリクエストされることがほとんどだったと思われます。
認証がかかっていない状態で正しい"go-import"情報を返してしまうのは情報漏洩です。この正しい、というのは、存在しないパスやgo moduleでないパスにリクエストが来た場合にエラーを返すというのも含まれています。gitlabの立場からするとsubmoduleやsubgroupの存在を無視して正しかろうが正しくなかろうがオウム返しの返答を行うしかありません。
GO 1.24からGOAUTH環境変数を設定することであらゆるhttp requestが認証可能になっていますが、これを前提とすると既存のワークフローがすべて破壊されてしまいます。そのため後方互換性を考慮してこの挙動が変えられることはないと筆者は考えます。
下記のソースより、gitlabを含めてgo toolにとってパスパターンが既知でないものは末尾の// General syntaxのところにマッチします。
つまり、${domain}/${organization}/${project}だと思って処理されます。
上記のgo-getの挙動を組み合わせると、2階層目のグループがgit repositoryと思われてgit cloneの対象になってしまいます。
ただし、regexpを見るとわかる通り\.(?P<vcs>bzr|fossil|git|hg|svn))にマッチすればそのパスまでがVCSのターゲットだとして処理されます。
ということで、privateかつ2階層以上のグループを持つ場合はmodule nameのrepository部分に.gitをつけましょう。
vcs suffixをつけたくない場合には?
もし仮にmodule nameにVCS suffixをつけたくないとしたら、好ましい解決方法は
- サブグループを使わない
- vanity import pathを返すサーバーを運用する
- private repositoryを参照できるgo module proxyを運用する
だと思います。
筆者はVCS suffixをつける方法に甘んじているためいずれも試していないことに注意願います。
- サブグループを使わない:
- 単純ですが、サブグループを使わないだけでも解決します。
- 前述通り
VCSsuffixがない場合、${domain}/${organaization}/${project}というフォーマットとして解釈され、このパスにまず?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が正しく取れれば)あとは成功するでしょうから、これでもうまくいくと思われます。 -
Go 1.25からサブパスがmodule rootとなる場合の
go-importのフォーマットの考慮が追加されたため、mono repo運用をする場合はこれへ追従が必要だと思います。
- go module proxyを運用する:
- 最も正道で最も大変な方法と思われます。
-
module proxyは
GOPROXYprotocolを実装したHTTPサーバーです。 - 独自実装を用いるかgithub.com/goproxy/goproxyがgoでgo module proxyを実装しているのでこれを用いるかどちらかがいいと思います
- このmodule proxy server自体にauthが必要ですのでGOAUTHを各clientに設定してもらうか、イントラからしかアクセスできないようにするかする必要があります。これはこれで大変ですね。
module proxyを運用したい別の理由があるなら話は違いますが、VCS suffixをつけてしまうのが一番楽です。
実行ファイルをつくってビルド・実行する
いきなり例外: exampleではサブパス以下でgo mod init
以下の手順ではgit repositoryの直下じゃなくてサブディレクトリにmoduleを作っています。これはこの一連の記事群のためのスニペットをまとめて同じrepositoryに置きたい筆者の都合です。
なので読者はパスはいい感じに読み替えて都合のいいパスで実行してください。
mkdir starting-projects
cd starting-projects
go mod init github.com/ngicks/go-example-basics-revisited/starting-projects
エントリーポイントの作成・ビルドして実行
エントリーポイントを作成します。
mkdir -p cmd/example
touch cmd/example/main.go
ファイルの中身を以下のようにします。
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
main packageのmain関数がエントリーポイントとなります。
エントリーポイントとはプログラムが実行されるときの開始地点となる場所のことをさします。
(プログラム実行時に最初に実行される関数というわけではありません。厳密に言うと、以下はmainより先に実行されます: (1)var foo = bar()のようなpackage toplevel scopeで実行される関数、(2)init関数(func init() {})、(3)Goそのもののランタイム、(4)CGOかつコンパイラがGCCの場合の__attribute__((constructor))のつけられた関数など)
このmain packageは以下のコマンドでビルドすることができます。
go build ./cmd/example
実行すると./example(GOOS=windowsの場合./example.exe)が出力されます。
もしくは以下のコマンドでコンパイルして実行することができます。
$ go run ./cmd/example
Hello world
go runはOS依存のtmpディレクトリにビルドして実行するショートハンド的コマンドで、毎回ビルドしてしまうので複数回実行したい場合はgo buildしたほうが良いこともあります。
go buildに渡すパスは必ず./でprefixする
ちなみに、以下のように./を省略してしまうとダメです。
$ 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)以下にあるかのように解決されてしまうからです。
以下のように拡張付きで指定した場合は場合はエラーなく実行できますが、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
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名で指定するとよいでしょう。
go.modの編集のしかた
go.modはgo get ...やgo mod edit ...コマンドで編集します。
go mod init実行後に以下のファイルが作成されたと思います
module github.com/ngicks/go-example-basics-revisited/starting-projects
go 1.25.2
このファイルが、go moduleの
- 名前(というかパス)
- version
- toolchain
- 依存するほかの
go module - 依存する
tool
などを記録するファイルとなります。
pyproject.toml、package.json、deno.jsonなどと近しいものです。
このファイルはgo getやgo mod tidyなどのコマンドに編集してもらうことになるので、手で編集することは少ないです。
go versionのfix release(1.25.2の末尾の.2)が0以外だと少々具合が悪いので編集します。
go mod edit -go=1.25.0
するとgo.modの内容は以下のように変更されます。
module github.com/ngicks/go-example-basics-revisited/starting-projects
-go 1.25.2
+go 1.25.0
Goのmajor release(Go 1.24やGo 1.25のような)はAPI追加、構文の追加、たまにエッジケースの挙動が破壊的に変更されますから、これは重要な観点です。他方、fix releaseはセキュリティーにかかわるfix以外では挙動の変更は起こらないことになっています。
別に動作するにもかかわらずfix releaseが古いgo moduleからgo getできなくなるため、そのfix verdsionで修正された挙動に依存しない限りにおいては.0を指定しておくほうが良いのではないかと思います。
std libraryはビルドするときのtoolchainのものが使われるため、ビルドする側の設定次第で1.25.4でもビルドできますのでgo.modでは常に.0を指定していても問題ないはずです。
ほかのコマンドは
go help mod
で確認できます。
go mod download
go mod tidy
ぐらいを覚えておけばいいかな。
packageを分ける
前述のとおり、moduleは複数のpackageに分割されます。
概要
- directory = package
- package内ではnamespaceが共有される: 別ファイル間で関数や型などの名前をimportせずに参照しあえるし、同名のものがあるとエラー
- 1つのdirecotryは1つのpackageしか書けない。
- ただし例外として
_testsuffixをつけたテスト用のpackageを同じdirectory内に定義できる(e.g.package fooに対してpackage foo_test)。-
foo_testpackageはfooとは別のpackage扱いとなり公開されたシンボルにしかアクセスできない -
fooでテストを記述するとcyclic importが起きる場合にfoo_testにテストを分けるとよい。
-
- packageは関心に応じて分割するとよい
-
Goの慣習とGo teamのおすすめ的には、packageは関心をもとに分割するのが良いとされる - まずはpackageは分割せず、同一package内にすべて定義し、不都合が生じ始めたら分割したらよい、と言われている。
- もちろんもとから関心が別れる点が明確であれば先だってpackageを分割しておいても特段問題はないだろう。
-
- packageとdirecotryの名前は一致しているのが望ましい。
- importされたpackageは、packageの名前がidentifier(変数とか関数の名前のようなもの)として機能し、それを通じてアクセスされる。
- つまり、directory nameとpackage nameの不一致は可読性が悪い
-
goplsが自動的に補完する機能があるため問題自体は起きにくい(i.e.import (actual_name "path/to/module")のactual_nameを勝手に付け足す)
-
- 例外:
- packageの名前がdirectoryの名前のsuffixであるとき(
github.com/charmbracelet/bubbleteaのpackage nameはtea)-
semanticTokensの設定を有効にしていると、packageの名前部分だけ色が変わる.
-
- version suffxi(
math/v2のpackage nameはmath)
- packageの名前がdirectoryの名前のsuffixであるとき(
- 慣習的にpackageの名前は1語で短いものが良いとされる。
- 長い名前 => package階層設計がうまくいっていないことの兆候。
- 複数単語を含む場合でも
_や-などを用いない(e.g.some_packageではなくsomepackage)- 前述通りpackageの名前がそれにアクセスするためのidentifierとなる一方で、
Goのpackage pathはURLとして用いられることがあるため:-
-はidentifierに含められない - 変数名の慣習は
PascalCase/camelCase=>_が含まれるのは変数名の慣習と不一致 - 大文字・小文字を区別しないファイルシステムが存在する(=Windows)
- URLは慣習的に小文字に丸め込むのが普通
-
- 前述通りpackageの名前がそれにアクセスするためのidentifierとなる一方で、
複数packageを作ってimportする
同じpackage内のファイルはnamespaceを共有しています: つまり別のファイルに同名の関数は定義できないし、別のファイルの関数や変数を利用可能です。
mkdir pkg1 pkg2
touch pkg1/some.go
touch pkg2/other.go
ファイルを以下のようにします
package pkg1
var Foo = "foo"
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パスを書くことで、インポートします。
cyclic importはエラー
Go moduleは、循環インポートを許しません。つまり
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)
go get
以下のコマンドドキュメントを参考にすると
go get <<fully-qualified-package-path>>
で、指定されたGo moduleが取得され、go.modとgo.sumが編集されます。
例えば
$ go get github.com/ngicks/go-iterator-helper
go: added github.com/ngicks/go-iterator-helper v0.0.23
を実行すると以下のようにgo.modとgo.sumにmodule情報が追記されます。
module github.com/ngicks/go-example-basics-revisited/starting-projects
go 1.25.0
+ require github.com/ngicks/go-iterator-helper v0.0.23 // indirect
+github.com/ngicks/go-iterator-helper v0.0.23 h1:XtiWqVD9grfbs7yCuGEX5f5gC3Oud/0pq2rBM9PVs0M=
+github.com/ngicks/go-iterator-helper v0.0.23/go.mod h1:g++KxWVGEkOnIhXVvpNNOdn7ON57aOpfu80ccBvPVHI=
まだこのmoduleはこのプロジェクトのどこからも使われていないので// indirectがつけれています。
importで各ソースコードにmoduleを導入して使用できるようになります。
goplsの設定でcompleteUnimportedが有効にされていると、可能な場合importに書かれていない内容でも補完がかかります(e.g. import clauseがないファイルでfmt.と打つとfmt.Printlnなどがサジェストされ、選ぶとimport "fmt"が追記される)ので、見た目に反してこの内容を書くのは面倒ではありません。(初めて導入したモジュールとかは補完されないことがある。)
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.sumへ反映
この時点でgo mod tidyを実行します。
go mod tidy
module github.com/ngicks/go-example-basics-revisited/starting-projects
go 1.25.0
-require github.com/ngicks/go-iterator-helper v0.0.23 // indirect
+require github.com/ngicks/go-iterator-helper v0.0.23
+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.23 h1:XtiWqVD9grfbs7yCuGEX5f5gC3Oud/0pq2rBM9PVs0M=
github.com/ngicks/go-iterator-helper v0.0.23/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を実行しておくほうがよいでしょう。実際にコード中でimportされる前にgo mod tidyを読んでしまうとgo.modから記述が消されてしまうので気をつけましょう。
go get時に@でqueryを指定する
上記の例ではgo get時にversionを指定していないため、適当な最新バージョンが選ばられるようです。
下記のドキュメントにある通り、versinon queryによってversionの指定を行うことができます。
大体の場合下記のいずれかの方法で行うことになると思います
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 tagはvでprefixされたSemantic Versioning 2.0形式であればgo.modにそのバージョンで記載されます(参照)。sem ver形式でなくてもよいですが、その場合はpseudo-versionというpre-release形式のversionにエンコードされて記載されます。
このpseudo-versionを直接指定しても取得できますが、指定したいrevisionの直前のsem verから1つ進んだversionのpre-releaseになる変換方式のようですので、これを直接指定するのは手間です。なので代わりに${git-commit-hash-prefix}などを使うとよいでしょう。
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
内容は何でもいいのでとりあえず以下のようにします。
package i0
const Yay0 = "yay0"
package i1
const Yay1 = "yay1"
package i2
const Yay2 = "yay2"
前述のとおり、internal/と同階層か、それより下からは参照できます。
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
+}
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してみます
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の構成例
基本的な構成
- 実行ファイルを作成するのが主眼となる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にふさわしい何かにする)
Pattern1: 空のトップディレクトリ
.
├── subdir
│ ├── some.go
│ ├── other.go
│ └── moreother.go
├── go.mod
└── go.sum
github.com/google/go-cmpの直下にcmp packageがあり、メインのロジックはすべてそこに入っているというパターン。
githubなどのrepositoryがそのままmodule pathとなってしまうため、名前かぶりを避けるためにgo-のようなprefixをつけると、importする時に非常に扱いづらい(gocmpにgoplsの自動補完によってrenameされるうえ、長くなる)。
そのため、十分にユニークな名前をトップディレクトリに与え、メインのロジックは呼びやすい名前を付けたサブディレクトリ以下に配置する。
Pattern2: pkgディレクトリ
.
├── pkg
│ ├── somepkg
│ │ └── files.go
│ └── otherpkg
│ └── files.go
├── go.mod
└── go.sum
github.com/moby/mobyやgithub.com/docker/composeで見られるパターン。
ほかのロジックからはある程度独立した関心を持つが、別のGo moduleに分けるほどではないものをここに置いたりする。
ほかの階層を汚さないのでスクリプトとかをいっぱい置くときはこういう構成にするといい感じになるときもあります。
Private repositoryからgo getする
上記の説明より、一般公開されない、つまり特別な認証が必要な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)。
前述しましたが、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 gitかつサブグループを使用する場合)module nameに.gitをつける
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 configでX11Forwarding 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-lfsはGit 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を使います。
- 使わない:
go generateで事足りるので特にtask runnerを使わない- やり方:
-
Generate Go files by processing sourceより、
//go:generate commandの文法でソース中に書かれたコメントがgo generateサブコマンドで実行可能です。 -
go generate ./...でcwd以下のすべての//go:generateが実行可能です。 - スクリプトも
goでinternal以下に実行ファイルを書くことで実装可能。
-
Generate Go files by processing sourceより、
- pros:
- すごくシンプル。
-
Goだけで完結。
- cons:
- タスクの依存関係などは記述が難しい。
- やり方:
-
make
- pros:
- unix系のシステムではpre-installedなことが多い
- 利用者が多い
- cons:
-
makeはtask runnerではないという批判 - windowsで動かすときハマりがち
-
- pros:
-
github.com/go-task/task
- pros:
-
Goで開発されているため、toolchainが入っていれば簡単にインストール可能 - cross-platform
- yamlで書ける
-
- cons:
- 管理すべきツールが増える
- 他で利用しないかもしれないツールへの習熟が必要
- pros:
-
deno task
- pros:
- cross-platform
- jsonで書ける
- daxと組み合わせて利用するとcross-platformなshellscriptみたいに書ける。
- cons:
- 管理すべきツールが増える
- 別言語への習熟が必要
- TypeScriptはできる人多いかなって思うからそこは問題ないかも・・・
- pros:
-
mise tasks
- pros:
- tomlとshellscript両方をサポート
-
watchでタスクをラップ可能 - すでに
miseでツールを管理している場合は追加のツール不要
- cons:
- うっかりshellscript taskを利用するとwindowsでうまく動かない可能性あり
- pros:
おわりに
private repositoryからgo getするのは特に躓いたのでまとめておきました。
Discussion