Go言語で画像処理CLIツールを作って学ぶ:基礎編
こんにちは!今回、Go言語で本格的な画像処理CLIツール「imgai」を開発したので、その過程で学んだGoの基礎知識を共有します。
なぜGoを選んだのか?
画像処理ツールを作るにあたって、Go言語を選んだ理由は明確でした:
Goの魅力:
- 圧倒的な実行速度 - コンパイル言語だから画像処理が爆速
- 学習コストが低い - シンプルな言語仕様で挫折しにくい
- 並行処理が超簡単 - goroutineで複数画像を並列処理
- デプロイが楽 - シングルバイナリで配布できる
- Apple Silicon対応 - M1/M2 Macで最適化されたバイナリが作れる
プロジェクト構成の基本
Goでは慣習的なディレクトリ構成があります。これを守ると、他のGopherが見ても理解しやすいコードになります:
imgai/
├── cmd/ # CLIコマンド
│ ├── root.go # ルートコマンド
│ ├── resize.go # リサイズコマンド
│ ├── convert.go # 変換コマンド
│ └── exif.go # メタデータ読み取り
├── pkg/ # 再利用可能なパッケージ
│ ├── image/ # 画像処理ロジック
│ ├── batch/ # バッチ処理
│ └── metadata/ # メタデータ処理
├── main.go # エントリーポイント
└── go.mod # 依存関係管理
なぜcmdとpkgに分けるの?
cmd: ユーザーインターフェース層。CLI固有のロジック。
pkg: ビジネスロジック層。他のプロジェクトでも再利用できる汎用的なコード。
この分離、最初は「面倒くさ!」って思ったんですが、後から機能を追加する時にめちゃくちゃ楽でした。
Go言語の基礎文法
1. パッケージとインポート
Goでは、すべてのコードはパッケージに属します:
package main // mainパッケージはエントリーポイント
import (
"fmt" // 標準ライブラリ
"os"
"github.com/spf13/cobra" // 外部ライブラリ
)
ポイント:
-
mainパッケージのmain関数が実行開始点 - インポートは
()でグループ化するとスッキリ - 未使用のインポートはコンパイルエラーになる(厳しい!)
2. 変数宣言の3パターン
Goには変数宣言の方法が複数あります:
// パターン1: 型を明示
var width int = 800
// パターン2: 型推論
var height = 600
// パターン3: 短縮記法(関数内のみ)
quality := 90
使い分け:
- パッケージレベル変数 →
var - 関数内の変数 →
:=(短くて楽) - ゼロ値で初期化 →
var i int(値は0)
3. 構造体(Struct)
Goはオブジェクト指向言語ではありませんが、構造体でデータをまとめられます:
// ResizeOptions holds options for resizing
type ResizeOptions struct {
Width int
Height int
Output string
}
// 使い方
opts := ResizeOptions{
Width: 800,
Height: 600,
Output: "result.jpg",
}
命名規則:
- 型名はPascalCase(先頭大文字で外部公開)
- フィールド名もPascalCase(公開したい場合)
- 小文字始まりはパッケージ内のみアクセス可能
4. エラーハンドリング
Goには例外機構がありません。エラーは戻り値で返します:
func ResizeImage(path string, opts ResizeOptions) error {
// ファイルを開く
img, err := imaging.Open(path)
if err != nil {
return fmt.Errorf("failed to open: %w", err)
}
// リサイズ処理
resized := imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
// 保存
if err := imaging.Save(resized, opts.Output); err != nil {
return fmt.Errorf("failed to save: %w", err)
}
return nil // 成功時はnilを返す
}
ポイント:
- エラーチェックは必須(無視するとバグる)
-
%wでエラーをラップすると呼び出し元でエラー原因を追跡できる - 成功時は
nilを返す
5. ポインタ
Goにはポインタがありますが、C/C++ほど複雑じゃないです:
// 値渡し(コピーされる)
func modifyValue(opts ResizeOptions) {
opts.Width = 1000 // 元の値は変わらない
}
// ポインタ渡し(アドレスを渡す)
func modifyPointer(opts *ResizeOptions) {
opts.Width = 1000 // 元の値が変わる!
}
// 使い方
opts := ResizeOptions{Width: 800}
modifyPointer(&opts) // &でアドレスを渡す
fmt.Println(opts.Width) // 1000
いつポインタを使う?
- 大きな構造体 → ポインタ(コピーコスト削減)
- 値を変更したい → ポインタ
- 小さな構造体 → 値渡しでOK
実践例:画像リサイズ機能
理論だけじゃつまらないので、実際のコードを見てみましょう:
package image
import (
"fmt"
"github.com/disintegration/imaging"
)
// ResizeOptions holds options for resizing
type ResizeOptions struct {
Width int
Height int
Output string
}
// ResizeImage resizes an image based on options
func ResizeImage(inputPath string, opts ResizeOptions) error {
// 1. 画像を開く
img, err := imaging.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open image: %w", err)
}
// 2. 元のサイズを取得
bounds := img.Bounds()
origWidth := bounds.Dx()
origHeight := bounds.Dy()
// 3. リサイズ(アスペクト比維持)
targetWidth, targetHeight := calculateDimensions(
origWidth, origHeight,
opts.Width, opts.Height,
)
// 4. リサイズ実行
resized := imaging.Resize(
img,
targetWidth,
targetHeight,
imaging.Lanczos, // 高品質なアルゴリズム
)
// 5. 保存
if err := imaging.Save(resized, opts.Output); err != nil {
return fmt.Errorf("failed to save: %w", err)
}
fmt.Printf("✓ Resized: %dx%d → %dx%d\n",
origWidth, origHeight,
targetWidth, targetHeight,
)
return nil
}
// calculateDimensions calculates target dimensions
func calculateDimensions(origW, origH, targetW, targetH int) (int, int) {
if targetW > 0 && targetH > 0 {
return targetW, targetH // 両方指定
}
aspectRatio := float64(origW) / float64(origH)
if targetW > 0 {
// 幅だけ指定 → 高さを計算
return targetW, int(float64(targetW) / aspectRatio)
}
// 高さだけ指定 → 幅を計算
return int(float64(targetH) * aspectRatio), targetH
}
このコードのポイント:
-
関数は小さく -
calculateDimensionsを分離 -
エラーはラップ -
%wで元のエラーを保持 - コメントは英語 - 国際的なプロジェクトの慣習
- 早期リターン - エラーは即座に返す
go.mod - 依存関係管理
Goにはgo.modという依存関係管理ファイルがあります:
module github.com/hiroki-abe-58/imgai
go 1.21
require (
github.com/disintegration/imaging v1.6.2
github.com/spf13/cobra v1.8.0
)
便利なコマンド:
# 依存関係を追加
go get github.com/disintegration/imaging
# 使われていない依存を削除
go mod tidy
# 依存関係をダウンロード
go mod download
ビルドとテスト
ビルド
# 現在のOS向けにビルド
go build -o imgai
# クロスコンパイル(Windows用)
GOOS=windows GOARCH=amd64 go build -o imgai.exe
# Apple Silicon用に最適化
GOARCH=arm64 go build -o imgai
実行
# ビルドして実行
go run main.go
# ビルド済みバイナリを実行
./imgai resize photo.jpg --width 800
つまずきポイントと解決策
1. 「imported and not used」エラー
import (
"fmt" // これを使わないとエラー!
"os"
)
解決策: 未使用のインポートは削除するか、_で無視:
import (
_ "fmt" // 使わないけどインポート
)
2. 「undefined: function」エラー
小文字始まりの関数は、他のパッケージから見えません:
// ❌ 他のパッケージから呼べない
func resizeImage() { }
// ✅ 他のパッケージから呼べる
func ResizeImage() { }
3. 「nil pointer dereference」
ポインタがnilのまま使うとパニックします:
var opts *ResizeOptions
opts.Width = 800 // パニック!
// 正しい方法
opts := &ResizeOptions{}
opts.Width = 800 // OK
まとめ
Go言語の基礎を実践的なプロジェクトを通じて学びました:
学んだこと:
- ✅ パッケージ構成の設計
- ✅ 構造体とメソッド
- ✅ エラーハンドリング
- ✅ ポインタの使い方
- ✅ 依存関係管理
次回予告:
次の記事では、代表的なGoライブラリについて解説します。Cobra(CLI)、imaging(画像処理)、progressbar(UX改善)など、実務で使えるライブラリを紹介します。
それでは、Happy Coding!