目次

はじめに

本書の内容
免責事項

第1章 画像処理で遊ぼう!

1.1 はじめに
1.2 とりあえず、やってみる。
1.3 Imageを掘り下げ
1.4 色空間について
1.5 GIFの扱い方
1.6 ふたつの画像の合成(drawパッケージ)
1.7 まとめ
1.8 おまけ

第2章 Goでグラフを描写しよう

2.1 はじめに
2.2 この章の対象読者
2.3 環境
2.4 開発環境以外に必要なもの
2.5 gonum/plotライブラリについて
2.6 グラフについて
2.7 Goでグラフを作成してみよう:棒グラフ編
2.8 Goでグラフを描写してみよう:線グラフ編
2.9 Goでグラフを描写してみよう:箱ヒゲ図編
2.10 番外編
2.11 おわりに

第3章 GoでCLIを作ろう!

3.1 CLIってなに
3.2 cobraを使ってCLIを作ろう!
3.3 実装してみよう

第4章 TinyGo で WebAssembly

4.1 はじめに
4.2 WebAssemblyでHello World!
4.3 JavaScriptからGo関数の呼び出し
4.4 Go言語でDOM操作
4.5 エラーメッセージなどの対応
4.6 まとめ

第5章 実録!Goのクリーンアーキテクチャ

5.1 はじめに
5.2 クリーンアーキテクチャとは
5.3 この章のゴールとアプリケーション設計
5.4 環境構築
5.5 シンプルな実装
5.6 クリーンアーキテクチャにリファクタリング
5.7 さいごに

はじめに

本書の内容

 Women Who Go Tokyoは、Goを楽しむためのコンテンツをオムニバス形式で用意したものです。コンテンツを楽しむためは、いくつかの準備(Goやサードパーティパッケージのインストール)が必要になります。

 この本が読者のみなさまにとって、Goをさらに愛するきっかけとなりますように。

Women Who Goについて

 Women Who Goは、2014年にサンフランシスコで、女性・ジェンダーマイノリティの方々が集まるGoのグローバルコミュニティとして、設立されました。

 活動の目的は、多くの女性やジェンダーマイノリティの方々が、Goのコミュニティに安心して参加できるようにすることです。

 技術者のコミュニティは、最初は一人だと参加しづらいという声がしばしば聞かれます。Women Who Goは、仲間をみつけることができる機会を提供したいと考えています。また、一人だとどうすればよいかわからないことを質問し合うなど、協力し合いながら、楽しくGoについて学んでいきます。

 Women Who Goとして実施する活動の内容は各国自由で、ベルリンでは「Go In Action」の著者を招いてワークショップを行ったり、シカゴではGoの新バージョンがリリースされた際に、リリースパーティを行ったりしています。そして、そのイベントの多くは無料で開催されています。

 他国の活動については、Women Who GoのTwitterアカウント1から確認できます。

Women Who Go Tokyoの活動について

 Women Who Go Tokyoは、2016年6月、日本ではじめてのWomen Who Goのコミュニティとして設立されました。

 2016年6月に初のイベントを実施して以降、毎月1回、都内の会場に10人前後で集まり、Goについて学んでいました。2020年からは、オンラインでの開催に移行しています。

 主な内容は、プログラミング初学者向けのワークショップや読書会で、自分で手を動かしながら進めていきます。堅苦しくなく、できるだけゆるく和やかに気軽に参加できるような雰囲気にしたいと思い、運営しています。そして、イベントは無料で開催しています。

お問い合わせ先

 本書に関するお問い合わせ:

 ・https://web.womenwhogo.tokyo2

 ・admins@womenwhogo.tokyo3

免責事項

 本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。

 これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。

第1章 画像処理で遊ぼう!

 この章は、@saki_engineerが担当しています。

 また、ソースコードの挙動はすべて、"go version go1.15.5 darwin/amd64"で確認したものです。

1.1 はじめに

 画像処理をGoでやってみたい!となったときに、真っ先に見にいくのは、標準imageパッケージでしょう。新しいことを始めるワクワク感を胸いっぱいに抱いて、意気揚々とGoDocのページを開いたときの感想は───

図1.1: さっぱりわからんの図

 少なくとも私はそう思いました。なに「アルファ」って?RGBAとかNRGBAとかYCbCrとかいろいろあるけど、結局どれ使えばいいの??構造体とかメソッドの定義だけじゃなくて、具体的にやりそうな処理の実装例とか載っけといてよーーーー!と。

 この章では、「Goで画像をいじってみたいけど、そもそもの用語類が全然わからない」という方々向けに、imageパッケージでできることやその詳細、理解のために必要な用語類1を解説します。

1.2 とりあえず、やってみる。

 画像の加工をGoで行うためにはどうしたらいいのか、まずはグレースケール変換を例にとって、全体図を見てみましょう。2

リスト1.1: グレースケール変換の全体図

import (
        _ "image/jpeg"
        _ "image/png"
        // (略)
)

file, _ := os.Open("pic.png")
defer file.Close()

img, _, _ := image.Decode(file)

bound := img.Bounds()
NewImg := image.NewGray(bound)
for y := bound.Min.Y; y < bound.Max.Y; y++ {
    for x := bound.Min.X; x < bound.Max.X; x++ {
        NewImg.Set(x, y, img.At(x, y))
    }
}

newfile, _ := os.Create("new.png")
defer newfile.Close()
png.Encode(newfile, NewImg)

 次に、このコード全体で何をやっているのか?というところを詳しく見ていきましょう。

1.2.1 画像を読み込む

 読み込んだファイルをGoで画像として扱えるようにするためには、Fileオブジェクトからimage.Imageインターフェースに変換してあげる必要があります。このos.Open()で得たFile型を、image.Imageインターフェースにする作業をデコードといいます。

リスト1.2: jpeg,pngどちらにも対応する

import (
        _ "image/jpeg"
        _ "image/png"
        // (略)
)

file, _ := os.Open("pic.png")
defer file.Close()

img, _, _ := image.Decode(file)

 ここでは、imageパッケージ内に存在するimage.Decode()関数を用いています。このimage.Decode()関数を用いる場合は、使いたいファイル拡張子に応じたパッケージのインポート宣言をする必要があります。今回の場合、image/jpegimage/pngをインポートしています。よって、jpegファイルとpngファイルのデコードができるようになっています。

 image.Decode()ではなく、image/pngimage/jpegパッケージ内に存在する、個別のデコード関数を直接用いることも、一応できます。

リスト1.3: image/pngのデコード関数を用いた例

import (
        "image/png"
        // (略)
)

file, _ := os.Open("pic.png")
defer file.Close()

img, _ := png.Decode(file)

 しかし、読み込むファイルがpngではなくなった場合、コードの書き換えが必要です。image.Decode()を使用すれば、画像ファイルの拡張子が変わったり、不確定である場合にも、インポート宣言を増やすだけで対応可能です。よって、特別な状況でない限り、image.Decode()の使用が望ましいでしょう。

image.Decodeの利用に、どうしてjpeg/pngのインポート宣言が必要なのか?

 image.Decode()の実行時に、読み込みたい拡張子のパッケージのインポート宣言が行われていないと、エラーが出ます。

リスト1.4: エラーがでる

img, _, err := image.Decode(file)
fmt.Println(err)
// image: unknown format

 ここでいう"image: unknown format"とは、どういうことなのでしょうか。image.Decode()の説明文を見てみます。3

Decode decodes an image that has been encoded in a registered format.Format registration is typically done by an init function in the codec- specific package.

訳: Decodeでは、登録されたフォーマット形式(registered format)の画像をデコードします。  formatへの登録は、特定のパッケージのinitによって行われます。

 エラー文の"unknown format"というのは、この「登録されたフォーマット形式」にデコードを試みた、ファイル拡張子のものがないということです。そして、その登録は「パッケージのインポート時のinit関数の実行」で行われます。

リスト1.5: image/jpegのinit()

func init() {
    image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}

 これは、image/jpegをインポートした際に実行されるinit関数です。4ここでは、image.RegisterFormat関数を使って、jpegをデコードするための関数jpeg.Decode(第三引数)を登録しています。

 さて、今まで出てきた「フォーマット」というのは、一体何者なのでしょうか。一言で説明するならば、「画像バイナリファイルのプレフィックスパターンと、デコード設定・関数を対応付けたもの」です。

 具体例で理解するために、ここでもう一度image.RegisterFormat関数を使って、jpegフォーマットを登録している様子を見返してみましょう。あれは、「画像ファイルのプレフィックスが\xff\xd8(第二引数)で始まるものをjpeg(第一引数)として認識して、それがimage.Decodeimage.DecodeConfig関数の引数として渡された場合には、jpegパッケージ内のDecode関数とDecodeConfig関数を用いる」という対応付けを登録しているのです。

 各パッケージのinit()を用いて登録されたフォーマットは、imageパッケージで定義されているグローバル変数atomicFormatsに保存されます。そして、image.Decode()実行時に、渡されたファイルのプレフィックスと同じものが、atomicFormatsに登録されているフォーマットに含まれているか、探す処理が行われます。5

リスト1.6: image.Decode()の内部処理

func Decode(r io.Reader) (Image, string, error) {
        rr := asReader(r)
        f := sniff(rr)    // どの登録フォーマットと一致するか探す処理
        if f.decode == nil {
                return nil, "", ErrFormat  // 見つからなかったらerr
        }
        m, err := f.decode(rr) // デコード実行
        return m, f.name, err
}

// どの登録フォーマットと一致するか探す処理
func sniff(r reader) format {
    // atomicFormatsの中身を取得
        formats, _ := atomicFormats.Load().([]format)
        for _, f := range formats {
        // デコードしたいファイルのプレフィックスを取得
                b, err := r.Peek(len(f.magic))

        // 登録フォーマットのプレフィックスと比較
                if err == nil && match(f.magic, b) {
                        return f // 一致していれば、その拡張子と判断
                }
        }
        return format{}
}

 この実装を見ればわかるとおり、実はimageパッケージのDecode関数は「フォーマット登録された関数の中から、どれを使うのが適切なのか?」という判断しか行っていないのです。つまり、実際にファイルをデコードする処理自体は、image/pngimage/jpegといった個別のパッケージ上で実装されており、大元のimageパッケージではそれを受け取って使う仕組み(image.RegisterFormat関数)を整えているだけなのです。

 このように、実際のデコード処理を別パッケージに移譲する仕組みをとった理由として考えられるのは、先ほど述べたとおり「拡張子不明の状態でもDecodeできるようなコードを書けるようにする」というだけではなく、フォーマットの統一性を保つという側面もあると推測されます。これは、image.Decode()image.DecodeConfig()の返り値をそれぞれimage.Imageや image.Configという型で指定することで、個別パッケージで実装されるDecode関数・DecodeConfig関数も同様のフォーマットで作られることになります。実際に、標準パッケージではない"golang.org/x/image/bmp"6では、拡張子BMPの画像をDecodeする関数を提供していますが、このDecode関数も、他同様の返り値を持ちます。こうすることで、「どんな拡張子であっても同じように扱える」という、プログラマーにとって優しい世界が構築されます。Goの標準パッケージというのは、ただ単によく使われる、もしくは基本的な機能を提供するというだけではなく、Goコミュニティの中でのものの扱い方に関する指針を固め、示す役割もあるということが、この例からよくわかりますね。

1.2.2 画像にしたい処理を加える

 image.Imageインターフェースに元画像を変換したあとにやることは、大きく分けてふたつです。

 1.新画像用のimage.Imageを用意する

 2.元画像のデータを使いながら、新画像のピクセルを埋める

 たとえば、冒頭に挙げたグレースケール変換の例の場合は、次のような処理になっています。

リスト1.7: 画像変換の仕方(Gray)

img := getImage() // 元画像のimageを用意

// 新画像のimageを用意する。
bound := img.Bounds()
NewImg := image.NewGray(bound)

// 新画像のimageを1ピクセルずつ埋める
for y := bound.Min.Y; y < bound.Max.Y; y++ {
    for x := bound.Min.X; x < bound.Max.X; x++ {
        NewImg.Set(x, y, img.At(x, y))
    }
}

 まずは、image.NewGrayでグレースケールのimageを用意します。その際に、作る画像の大きさを指定する必要があるので、元画像の大きさをBoundメソッドで取得して、それをimage.NewGrayの引数として渡しています。

 その後、forループを回して、新しい画像の各ピクセルを何色にするのかをひとつひとつ指定していきます。具体的には、グレースケールimageのSetメソッドを使うことで、元画像のピクセルデータをグレースケールの色に直しています。

1.2.3 画像の出力

 新しい画像のimageを作り終わったら、それをファイルに出力します。この作業のことをエンコードといいます。

 デコードの際は、image.Decodeを使用することで、どの画像拡張子であったとしても一律に読み込むことができていました。ですが、エンコードにはそのような「どのフォーマットでもOK」という関数は存在しません。pngとして出力したいのならばimage/pngpng.Encodeを、jpegとして出力したいのならばimage/jpegjpeg.Encodeを使用します。

リスト1.8: pngでエンコードする

newfile, _ := os.Create("new.png")
defer newfile.Close()

png.Encode(newfile, NewImg)

リスト1.9: jpegでエンコードする

newfile, _ := os.Create("new.jpeg")
defer newfile.Close()

jpeg.Encode(newfile, NewImg, &jpeg.Options{Quality: 75})

jpegとpngの違い

表1.1: jpegとpngの比較


jpeg png
色空間 RGB, CMYK, YCbCr RGBのみ
圧縮方法 非可逆 可逆
背景 透明不可 透明可

 jpegの特徴は、多彩な色空間と圧縮方法です。圧縮の仕方は「数ピクセルをブロックとして扱い、一番近い色ひとつに置き換える」というやり方なので、圧縮後に元に戻すことはできません。また、圧縮処理の特性上、「色の境界部分がぼやける」ような結果になります。そのため、色変化がはっきり・くっきりしている人工物の描画には不向きです。逆に、グラデーションのような色変化は扱いやすく、写真の画像はこのjpeg形式で保存されます。

図1.2: jpegでの圧縮イメージ

 pngの特徴は、透過処理と可逆圧縮が可能なことです。そのため、何回も加工するような画像や、イラスト・ロゴの保存に適しています。ただ、多彩な色を表現しようとすると、ファイルサイズが大きくなるという欠点があります。

図1.3: pngでの圧縮イメージ

1.2.4 結果

 グレースケールにした結果は次の画像のとおりです。

図1.4: グレースケールにした
試し読みはここまでです。
この続きは、製品版でお楽しみください。