0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語で画像処理CLIツールを作って学ぶ:基礎編

Last updated at Posted at 2025-11-09

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
}

このコードのポイント:

  1. 関数は小さく - calculateDimensionsを分離
  2. エラーはラップ - %wで元のエラーを保持
  3. コメントは英語 - 国際的なプロジェクトの慣習
  4. 早期リターン - エラーは即座に返す

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!

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?