見出し画像

Go+Echoで簡単なWeb APIを作ってみる


こんにちは、株式会社SHIFT、自動化エンジニアの水谷です。

Go言語は、いろいろな用途に使われるプログラミング言語なのですが、最近はWebアプリのバックエンド、つまりWeb APIの実装に使われることも多いようです。

Go用のWebアプリケーションフレームワーク(WAP)にもいくつか種類があるのですが、今回は軽量でネット上にも情報がたくさんあるEchoを使って簡単なWeb APIを作ってみたいと思います。

SHIFTエンジニアが学びながら解説するGo言語というマガジンができました。過去の記事もこちらから読めますので、もし本記事に興味をもっていただけましたら、マガジンの方も是非見ていただければと思います。

マンデルブロー集合の画像を返すAPI

前回の記事で、Fyneとゴルーチンを使って、下のようなマンデルブロー集合の画像を表示するアプリを作りましたが、今回はそのコードを流用して、マンデルブロー集合の画像をPNG形式で返すAPIを作成してみたいと思います。

画像1

APIは、マンデルブロー集合の計算範囲(実数部および虚数部)をパラメーターで指定できるようにし、出来上がった画像はPNG形式にエンコードし(JPEGでも良かったのですが、ちょっと細部が崩れるのでPNGにしました)、それをBASE64でテキスト化して返す、という仕様にします。

Echoのインストール

では、作っていきましょう。まずは適当なディレクトリを作成し、"go mod init"を実行します(名前も適当に付けます)。

> go mod init gomandel_backend

続いて、Echoを"go get"コマンドでインストールします。

> go get -u github.com/labstack/echo

最小のWeb APIからスタート

では、まずHello World的なコードから始めましょう。ルート(/)をGETでアクセスすると、"Hello from Go Echo"という文字列を返すAPIのコードは下のようになります(ポートは8080を使っていますが、もちろん他の番号でもOKです)。なお、ファイル名はbackmain.goとしています。

package main

import (
   "net/http"

   "github.com/labstack/echo"
)

func rootHandler(c echo.Context) error {
	return c.String(http.StatusOK, "Hello from Go Echo")
}

func main() {
   e := echo.New()
   e.GET("/", rootHandler)
   e.Logger.Fatal(e.Start(":8080"))
}

main()関数の中もシンプルで分かりやすく、簡単ですね。echo.New()でEchoのインスタンスを作成し、e.GET()でハンドラーを定義して、あとはハンドラーの内容を記述していけばOK、ということになります。また、最後の行は、Webサーバーを指定したポートで実行し、エラーが発生したらログを記録する、という動作になります。

ビルドして、curlでアクセスして見ると、下のように期待通りの文字列が返ってきました。

> go build backmain.go
> curl http://localhost:8080
Hello from Go Echo

マンデルブロー集合APIの実装

では、このコードに/GetMapをGETでアクセスされたときのハンドラーを追加します。このハンドラーがマンデルブロー集合の計算を行って、画像データを返すAPIとなります。

まずは、main()に下の行を追加

	e.GET("/GetMap", getMapHandler)

そして、getMapHandlerという関数を作って実装していきます。

func getMapHandler(c echo.Context) error {
}

まずはパラメーター(Form Value)の受け取り部分ですが、このAPIは4つのパラメーターを受け取ります。

realmin: 実数部の最小値
realmax: 実数部の最大値
imaginarymin: 虚数部の最小値
imaginarumax: 虚数部の最大値

これを受け取るコードは下のようになります。

	realMin, err1 := strconv.ParseFloat(c.FormValue("realmin"), 64)
	realMax, err2 := strconv.ParseFloat(c.FormValue("realmax"), 64)
	imaginaryMin, err3 := strconv.ParseFloat(c.FormValue("imaginarymin"), 64)
	imaginaryMax, err4 := strconv.ParseFloat(c.FormValue("imaginarymax"), 64)
	if err1 != nil || err2 != nil || err3 != nil || err4 != nil ||
		realMin >= realMax || imaginaryMin >= imaginaryMax {
		return c.String(http.StatusBadRequest, "incorrect parameter")
	}

パラメーターは、c.FormValue()で取得できますので、それをstrconv.ParseFloat()に渡して、64bitの浮動小数点数にコンバートしています。

エラーチェックはちょっと横着(?)をして、4つのパラメーター全部が揃ってからまとめて行っています。何らかの問題があれば、ステータスコード400と"incorrect parameter"を返します。

そして、マンデルブロー集合の計算を行って画像を作成します。

	img := image.NewRGBA(image.Rect(0, 0, 800, 800))
	nGR := 4
	fchannel := make(chan int)
	defer close(fchannel)
	r := make([]int, nGR)
	for i := range r {
		go CalcGR(img, realMin, realMax, imaginaryMin, imaginaryMax, i, nGR, fchannel)
	}
	for range r {
		<-fchannel
	}

今回は簡単のため、ゴルーチンの数は4で固定としています。

そのゴルーチンはこんな感じになります(前回の記事で作ったものと同じです)。

func CalcGR(img *image.RGBA, rmin float64, rmax float64, imin float64, imax float64, offset int, nGR int, fchannel chan int) {
	for y := 0; y < 800; y++ {
		for x := offset; x < 800; x += nGR {
			var C = complex(rmin+(rmax-rmin)*float64(x)/800, imin+(imax-imin)*float64(y)/800)
			var V = C
			var it uint8 = 0
			for ; it <= 100; it++ {
				V = V*V + C
				if cmplx.Abs(V) > 2 {
					break
				}
			}
			img.Set(x, y, color.RGBA{uint8(cmplx.Abs(V) * 40), uint8(cmplx.Abs(V) * 120), it * 2, 255})
		}
	}
	fchannel <- 1
}

計算が終わればfchannelを通して通知しています。

あとは、PNG形式にして、それをさらにBASE64でエンコードして返す部分ですが、このような一見手間がかかりそうな操作も、下のように簡単に実装できます。

	var buff bytes.Buffer
	png.Encode(&buff, img)
	encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())
	return c.String(http.StatusOK, encodedString)

テスト実行

では、ビルドして実行してみます。

> go build backmain.go
> curl http://localhost:8080/GetMap
incorrect parameter
> curl http://localhost:8080/GetMap?realmin=-1&realmax=1&imaginarymin=-1&imaginarymax=1
iVBORw0KGgoAAAANSUhEUgAAAyAAAAMgCAIAAABUEpE/AACAAElEQVR4nOS9B5gb1fX/fXRUV1u93l2ve8G9m2Zs
bLCNAYMpxjTTCRgIvYZOQpJfEhIgCQRICBASCB1M79U2trFxw717vfb2rlXX6M77TL8zc2c00q4N+b/7zLPPnas7
o9FImvnoe773HM8v70PggecBiPhfXHgCcmcOq4ZdUWMybJVbv9X+mQ3TgXWp4WxvAHaPPnAP8hzwaeA5IEqDucqn
xR7Dapo1hu6kGjYP6R41PWTZY7VtTotwPm0GMB8ltnugVu+4GNNR4KKQFhehERPbMdMSF/6TuNKOi221kVD+i4uu
kRTbSWVJAEkpbbHBp4TGKTsx2QrJNki1QapDXELCwnUCFxaXCKQjyhHGlIOJw7T3MbofYjUQr4N4AySaINkC0q6m
...

パラメーターなしでcurlした場合は、期待通りに"incorrect parameter"が返ってきて、パラメーターを設定してcurlした場合は、BASE64っぽい文字列が返ってきたことがわかります。うまく動いていそうですね。

CORS対応

同一生成元制限を緩和するためにCORSの設定をしたい場合が多いと思いますので、少し触れておきます。Echoにおいてこれを行うには、まず"github.com/labstack/echo/middleware"をインポートします。

import (
  ...

	"github.com/labstack/echo/middleware"
)

そして、main()に以下の行を追加してください。

	e.Use(middleware.CORS())

これで別プロジェクトで作ったフロントエンドから(ローカルで)アクセスした場合でもエラーが出なくなります。

フロントエンドを作る

この記事の本題はAPIの実装なので、ここまでで終わりという感じなのですが、やっぱりAPIを呼び出して画像を表示するフロントエンドが欲しい、ということで、JavascriptとReactでチャチャっと作りました。

ポイントだけ書いておきますと、まず<img>タグを下のように用意しておきます。

        <img width="800" height="800" src={image} alt="no image..." />

ここで、imageはReact HookのuseStateを使って定義しておきます。

  const [image, setImage] = useState("");

そして、4つのパラメーターを入力できるフォームを作っておき、そのSubmitハンドラーでAPIのURLをfetchします。

   const queryParams = new URLSearchParams({realmin: realMin, realmax: realMax, imaginarymin: imaginaryMin, imaginarymax: imaginaryMax})
   fetch('http://localhost:8080/GetMap?' + queryParams , {method: 'GET', mode: 'cors'})
     .then((res) => { 
       return res.text()
     })
     .then((data) => {
         setImage("data:image/png;base64," + data)
     })
     .catch((reason) => {
         console.log(reason);
     })

これで、APIが正常に実行されれば、SetImageが呼ばれてimageに値がセットされるので、自動的に画像が表示されます。

ということで、こんなWebアプリになりました。

画像2

パラメーターはデフォルトで実数部が-2.0~1.0、虚数部が-1.5~1.5となるようにしています。このパラーメーターを変更して「GENERATE」ボタンを押すと、APIがコールされて、下のように画像が変わります。

画像3

――――――――――――――――――――――――――――――――――

最後にコード全体を載せておきますので、参考にしていただければ幸いです。

package main

import (
	"bytes"
	"encoding/base64"
	"image"
	"image/color"
	"image/png"
	"math/cmplx"
	"net/http"
	"strconv"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func CalcGR(img *image.RGBA, rmin float64, rmax float64, imin float64, imax float64, offset int, nGR int, fchannel chan int) {
	for y := 0; y < 800; y++ {
		for x := offset; x < 800; x += nGR {
			var C = complex(rmin+(rmax-rmin)*float64(x)/800, imin+(imax-imin)*float64(y)/800)
			var V = C
			var it uint8 = 0
			for ; it <= 100; it++ {
				V = V*V + C
				if cmplx.Abs(V) > 2 {
					break
				}
			}
			img.Set(x, y, color.RGBA{uint8(cmplx.Abs(V) * 40), uint8(cmplx.Abs(V) * 120), it * 2, 255})
		}
	}
	fchannel <- 1
}

func getMapHandler(c echo.Context) error {
	realMin, err1 := strconv.ParseFloat(c.FormValue("realmin"), 64)
	realMax, err2 := strconv.ParseFloat(c.FormValue("realmax"), 64)
	imaginaryMin, err3 := strconv.ParseFloat(c.FormValue("imaginarymin"), 64)
	imaginaryMax, err4 := strconv.ParseFloat(c.FormValue("imaginarymax"), 64)
	if err1 != nil || err2 != nil || err3 != nil || err4 != nil ||
		realMin >= realMax || imaginaryMin >= imaginaryMax {
		return c.String(http.StatusBadRequest, "incorrect parameter")
	}

	img := image.NewRGBA(image.Rect(0, 0, 800, 800))
	nGR := 4
	fchannel := make(chan int)
	defer close(fchannel)
	r := make([]int, nGR)
	for i := range r {
		go CalcGR(img, realMin, realMax, imaginaryMin, imaginaryMax, i, nGR, fchannel)
	}
	for range r {
		<-fchannel
	}

	var buff bytes.Buffer
	png.Encode(&buff, img)
	encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())
	return c.String(http.StatusOK, encodedString)
}

func rootHandler(c echo.Context) error {
	return c.String(http.StatusOK, "Hello from Go Echo")
}

func main() {
	e := echo.New()
	e.Use(middleware.CORS())
	e.GET("/", rootHandler)
	e.GET("/GetMap", getMapHandler)
	e.Logger.Fatal(e.Start(":8080"))
}

また、フロントエンドは、create-react-appで作成し、App.jsを下のように変更しました。

import './App.css';
import React, { useState } from "react";
import Box from "@material-ui/core/Box";
import Button from '@material-ui/core/Button';
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";

function App() {
 const [image, setImage] = useState("");
 const [realMin, setRealMin] = useState("-2.0");
 const [realMax, setRealMax] = useState("1.0");
 const [imaginaryMin, setImaginaryMin] = useState("-1.5");
 const [imaginaryMax, setImaginaryMax] = useState("1.5");
 const [isFirstTime, setIsFirstTime] = useState(true)

 function handleSubmit(e) {
   console.log("calling API")
   const queryParams = new URLSearchParams({realmin: realMin, realmax: realMax, imaginarymin: imaginaryMin, imaginarymax: imaginaryMax})
   fetch('http://localhost:8080/GetMap?' + queryParams , {method: 'GET', mode: 'cors'})
     .then((res) => { 
       return res.text() })
     .then((data) => {
         setImage("data:image/png;base64," + data)
     })
     .catch((reason) => {
         console.log(reason);
     })
 }

 if (isFirstTime) {
   setIsFirstTime(false)
   handleSubmit(null)
 }

 return (
   <div className="App">
     <Box m={2} pt={2} mx="auto" width={816} height={816} border={1} bgcolor="#c0c0c0" boxShadow={3}>
       <img width="800" height="800" src={image} alt="no image..." />
     </Box>
     <Box>
       <form>
         <Grid container justify="center">
           <Grid item xs={2}>
             <TextField
               name="RealMin"
               onChange={(e) => { setRealMin(e.target.value) }}
               label="real_min"
               variant="outlined"
               autoComplete="off"
               value={realMin}
             />
           </Grid>
           <Grid item xs={2}>
             <TextField
               name="RealMax"
               onChange={(e) => { setRealMax(e.target.value) }}
               label="real_max"
               variant="outlined"
               autoComplete="off"
               value={realMax}
             />
           </Grid>
           <Grid item xs={2}>
             <TextField
               name="ImaginaryMin"
               onChange={(e) => { setImaginaryMin(e.target.value) }}
               label="imaginary_min"
               variant="outlined"
               autoComplete="off"
               value={imaginaryMin}
             />
           </Grid>
           <Grid item xs={2}>
             <TextField
               name="ImaginaryMax"
               onChange={(e) => { setImaginaryMax(e.target.value) }}
               label="imaginary_max"
               variant="outlined"
               autoComplete="off"
               value={imaginaryMax}
             />
           </Grid>
           <Box pt={1} pl={1}>
             <Grid item xs={2}>
               <Button
                 color="primary"
                 variant="contained"
                 onClick={handleSubmit}
               >
                 GENERATE
               </Button>
             </Grid>
           </Box>
         </Grid>
       </form>
     </Box>
   </div>
 );
}

export default App;

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに自動化エンジニアとして入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)に出向中。RGAでは副社長という立場でありながら、エンジニアとしてAnsibleやOpenshiftに関する案件も担当。また、Ansibleの社内教育や、外部セミナー講師も行っている。
最近の趣味は電動キックボードでの散歩。

★ 過去記事をまとめたマガジン「SHIFTエンジニアが学びながら解説するGo言語」もご参照ください。