こんにちは。KOUKIです。とある企業でGo言語を使ったWebシステムの開発業務に従事しています。
ここ最近、goroutineの使い所について紹介してきました。
今回は、第3回目ということで、画像ファイルをサブネイル画像に変換するプログラムを紹介したいと思います。


ちなみに、Udemyの「Learn the Why’s and How’s of concurrency in Go.」で学習したものを私の解釈付きで記載しています!※オススメです
<目次>
ワークスペースの準備
1 2 3 4 5 6 7 8 9 |
mkdir workspace cd workspace/ touch main.go # imgsには、適当なjpgファイルを格納してください mkdir imgs mkdir thumbnail # 関連モジュールをインストール go get -u github.com/disintegration/imaging |
実装
最初は、goroutineを使わないパターンを実装し、最後にgoroutineで並行処理化します。画像を扱うプログラムはちょっと頭を使うので、ハンズオンで少しずつ説明したいと思います^^

決して皆様の頭が悪いからと言いたいわけではなく、あくまで私が説明しやすいからです!
ルートパスを受け取る
最初に、以下のようにルートパスを受け取るプログラムを実装します。
1 2 |
go run main.go <ルートパス> go run main.go imgs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package main import ( "fmt" "log" "os" "time" ) func getRootPath() string { if len(os.Args) < 2 { log.Fatal("ルートパスを指定する必要があります: 例) go run main imgs") } rootPath := os.Args[1] fmt.Printf("ルートパス: %s\n", rootPath) return rootPath } func main() { // 計測用 start := time.Now() // ルートパスを取得する _ = getRootPath() fmt.Printf("計測時間: %s\n", time.Since(start)) } |
パスを受け取るには、osパッケージのArgsメソッドを使用します。
以下のように実行します。
1 2 3 4 5 6 7 8 |
$ go run main.go imgs ルートパス: imgs 計測時間: 29.67µs # ルートパスを指定していない場合 $ go run main.go 2020/12/13 08:20:44 ルートパスを指定する必要があります: 例) go run main imgs exit status 1 |
ルートパスから画像ファイルパスを取得する
取得したルートパス内を走破して、イメージパスを取得したいです。
そのためには、filepath.Walkを使用します。このメソッドは、引数に渡したルートパスを走破し、中に格納されているイメージファイルのパスを取得することができます。
ちなみに筆者は1~40.jpgファイルをimgs内に保存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
func walkFiles(root string) error { err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { fmt.Printf("イメージファイルパス: %s\n", path) if err != nil { return err } // ファイルかどうかのチェック if !info.Mode().IsRegular() { return nil } // Todo: ファイルタイプがimage/jpegかチェック // Todo: 画像をサブネイル画像に変換 // Todo: サブネイル画像を保存 return nil }) if err != nil { return err } return nil } func main() { // 計測用 start := time.Now() // ルートパスを取得する root := getRootPath() walkFiles(root) fmt.Printf("計測時間: %s\n", time.Since(start)) } |
Todoコメントの箇所は、後ほど実装します。
このプログラムを実行してみましょう。
1 2 3 4 5 6 7 8 9 |
$ go run main.go imgs/ ルートパス: imgs/ イメージファイルパス: imgs/ イメージファイルパス: imgs/1.jpg ... イメージファイルパス: imgs/7.jpg イメージファイルパス: imgs/8.jpg イメージファイルパス: imgs/9.jpg 計測時間: 451.335µs |
OKですね。
ファイルタイプを取得
ファイルタイプを取得する関数を作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
func getFileContentType(filePath string) (string, error) { // ファイルパスからファイル情報を取得 file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() // ファイルタイプは最初の512byteが使用される buffer := make([]byte, 512) _, err = file.Read(buffer) if err != nil { return "", err } // ファイルタイプを取得 contentType := http.DetectContentType(buffer) fmt.Printf("contentType: %v\n", contentType) return contentType, nil } func walkFiles(root string) error { err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // ファイルかどうかのチェック if !info.Mode().IsRegular() { return nil } // ファイルタイプがimage/jpegかチェック contentType, _ := getFileContentType(path) if contentType != "image/jpeg" { return nil } // Todo: 画像をサブネイル画像に変換 // Todo: サブネイル画像を保存 return nil }) if err != nil { return err } return nil } |
私も初めて知ったのですが、ファイルパスからファイル情報を取得すると最初の512byteにファイルタイプが入るらしいです。
そして、この情報をhttp.DetectContentTypeに渡すとファイルタイプが取得できます。
実際にプログラムを実行してみましょう。
1 2 3 4 5 6 7 8 |
$ go run main.go imgs/ ルートパス: imgs/ contentType: application/octet-stream contentType: image/jpeg contentType: image/jpeg contentType: image/jpeg .... 計測時間: 15.082842ms |
OKですね。注意点としては、「image/jpeg」ファイルタイプのファイルしかこのプログラムは処理しません。
サブネイル画像に変換
続いて、画像情報をサブネイルに変換する関数を実装をします。画像変換には、3rdパーティのdisintegration/imagingを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func processImage(filePath string) (*image.NRGBA, error) { // イメージ画像を読み込む srcImage, err := imaging.Open(filePath) if err != nil { return nil, err } // 100px * 100pxのサブネイルを作成 thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos) return thumbnailImage, nil } func walkFiles(root string) error { err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { ... // 画像をサブネイル画像に変換 thumbnailImage, err := processImage(path) if err != nil { return err } // Todo: サブネイル画像を保存 return nil }) ... } |
サブネイル画像を保存
最後にサブネイル画像を保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func saveThumbnail(srcImagePath string, thumbnailImage *image.NRGBA) error { // 1.jpgなどファイル名を取得する fileName := filepath.Base(srcImagePath) dstImagePath := "thumbnail/" + fileName err := imaging.Save(thumbnailImage, dstImagePath) if err != nil { return err } fmt.Printf("%s -> %s\n", srcImagePath, dstImagePath) return nil } func walkFiles(root string) error { err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { ... // サブネイル画像を保存 err = saveThumbnail(path, thumbnailImage) if err != nil { return err } return nil }) ... return nil } |
サブネイルデータを「thumbnail」ディレクトリ直下に保存するプログラムを実装しました。
プログラムを実行してみましょう。
1 2 3 4 5 6 7 8 |
$ go run main.go imgs/ ルートパス: imgs/ imgs/1.jpg -> thumbnail/1.jpg ... imgs/7.jpg -> thumbnail/7.jpg imgs/8.jpg -> thumbnail/8.jpg imgs/9.jpg -> thumbnail/9.jpg 計測時間: 6.530700563s |
OKですね。現状は完了までに6.530700563sの時間を要しますが、goroutineを使って時間を短縮してみましょう。
並行処理
さて、いよいよgoroutineを使って、並行処理を実装していきたいと思います。
今回は、walkfile(ディレクトリ走破) / processImage(画像の変換) / saveImage(画像の保存)のように工程が明確に別れている処理なので、goroutineの実装パターンの一つであるパイプライン処理が有効そうです。
パイプライン処理については、以前記事にしていますので、よかったら参考にしてください。
1 2 3 4 |
<Image> // Pipeline // walkFiles -------> processImage -------> saveImage // (path) (path) |
上記のイメージの通り、パイプラインの最初のステージは、walkFilesになります。
そのため、パイプラインをセットする関数を作成し、そこからwalkFilesを呼び出しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func setupPipeLine(root string) error { done := make(chan struct{}) defer close(done) // 最初のパイプライン walkFiles(done, root) return nil } func main() { start := time.Now() root := getRootPath() err := setupPipeLine(root) if err != nil { log.Fatal(err) } fmt.Printf("計測時間: %s\n", time.Since(start)) } |
done Channelは、goroutineの処理パターンの一つで、パイプライン処理に置いて処理完了を示すシグナルを送るために使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) { paths := make(chan string) errc := make(chan error, 1) go func() { defer close(paths) errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } contentType, _ := getFileContentType(path) if contentType != "image/jpeg" { return nil } select { case paths <- path: case <-done: return fmt.Errorf("walk was canceld") } return nil }) }() return paths, errc } func setupPipeLine(root string) error { ... // 最初のパイプライン paths, errc := walkFiles(done, root) return nil } |
walkFiles関数には、新たにdoneチャネルを引数に渡し、戻り値としてイメージ画像パスとエラーのChannelを指定しました。
次は、processImageのパイプライン化です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
// 処理結果を格納するstructを用意 type result struct { srcImagePath string thumbnailImage *image.NRGBA err error } func processImage(done <-chan struct{}, paths <-chan string) <-chan *result { results := make(chan *result) thumbnailer := func() { for path := range paths { srcImage, err := imaging.Open(path) if err != nil { select { case results <- &result{path, nil, err}: case <-done: return } } thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos) select { case results <- &result{path, thumbnailImage, nil}: case <-done: return } } } const numThumbnailer = 5 var wg sync.WaitGroup wg.Add(numThumbnailer) // 5回くらい回していれば、後から入ってきたpathも処理できると思われ for i := 0; i < numThumbnailer; i++ { go func() { thumbnailer() wg.Done() }() } go func() { wg.Wait() close(results) }() return results } func setupPipeLine(root string) error { done := make(chan struct{}) defer close(done) // 最初のパイプライン paths, errc := walkFiles(done, root) // secondステージ results := processImage(done, paths) return nil } |
result Structには画像ファイルの処理結果を格納しています。
また、最初のパイプラインで画像のパスをpathsチャネルで取得しますが、これはgoroutineで動かしているので、channelには画像ファイルパスが逐次送られてくるようになっているため、5回ほどループを回し、pathを処理しています。
最後は、画像保存処理の呼び出しをパイプライン化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func setupPipeLine(root string) error { done := make(chan struct{}) defer close(done) // 最初のパイプライン paths, errc := walkFiles(done, root) // secondステージ results := processImage(done, paths) // thirdステージ for r := range results { if r.err != nil { return r.err } saveThumbnail(r.srcImagePath, r.thumbnailImage) } if err := <-errc; err != nil { return err } return nil } |
非常に長くなりましたが、これで完成です。
プログラムを実行して見ましょう。
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go imgs/ ルートパス: imgs/ imgs/1.jpg -> thumbnail/1.jpg imgs/11.jpg -> thumbnail/11.jpg imgs/13.jpg -> thumbnail/13.jpg ... imgs/34.jpg -> thumbnail/34.jpg imgs/37.jpg -> thumbnail/37.jpg imgs/33.jpg -> thumbnail/33.jpg 計測時間: 3.043700349s |
結果は、「計測時間: 3.043700349s」でした。結構早くなりましたね!
おわりに
goroutineに加えてパイプラインの概念が入ってくると少し難しく感じます。
並行処理前のコードを何度か並列化することで、徐々に慣れていくことをオススメします!
それでは、また!
Go記事まとめ
関連図書
実装コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
package main import ( "fmt" "image" "log" "net/http" "os" "path/filepath" "sync" "time" "github.com/disintegration/imaging" ) type result struct { srcImagePath string thumbnailImage *image.NRGBA err error } func getRootPath() string { if len(os.Args) < 2 { log.Fatal("ルートパスを指定する必要があります: 例) go run main imgs") } rootPath := os.Args[1] fmt.Printf("ルートパス: %s\n", rootPath) return rootPath } func getFileContentType(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() buffer := make([]byte, 512) _, err = file.Read(buffer) if err != nil { return "", err } contentType := http.DetectContentType(buffer) return contentType, nil } func processImage(done <-chan struct{}, paths <-chan string) <-chan *result { results := make(chan *result) thumbnailer := func() { for path := range paths { srcImage, err := imaging.Open(path) if err != nil { select { case results <- &result{path, nil, err}: case <-done: return } } thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos) select { case results <- &result{path, thumbnailImage, nil}: case <-done: return } } } const numThumbnailer = 5 var wg sync.WaitGroup wg.Add(numThumbnailer) for i := 0; i < numThumbnailer; i++ { go func() { thumbnailer() wg.Done() }() } go func() { wg.Wait() close(results) }() return results } func saveThumbnail(srcImagePath string, thumbnailImage *image.NRGBA) error { fileName := filepath.Base(srcImagePath) dstImagePath := "thumbnail/" + fileName err := imaging.Save(thumbnailImage, dstImagePath) if err != nil { return err } fmt.Printf("%s -> %s\n", srcImagePath, dstImagePath) return nil } func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) { paths := make(chan string) errc := make(chan error, 1) go func() { defer close(paths) errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } contentType, _ := getFileContentType(path) if contentType != "image/jpeg" { return nil } select { case paths <- path: case <-done: return fmt.Errorf("walk was canceld") } return nil }) }() return paths, errc } func setupPipeLine(root string) error { done := make(chan struct{}) defer close(done) // 最初のパイプライン paths, errc := walkFiles(done, root) // secondステージ results := processImage(done, paths) // thirdステージ for r := range results { if r.err != nil { return r.err } saveThumbnail(r.srcImagePath, r.thumbnailImage) } if err := <-errc; err != nil { return err } return nil } func main() { start := time.Now() root := getRootPath() err := setupPipeLine(root) if err != nil { log.Fatal(err) } fmt.Printf("計測時間: %s\n", time.Since(start)) } |
最近のコメント