Go

Go言語 – echo

こんにちは、イサムです!

本記事では、Go言語のWebフレームワークであるechoについてまとめていきます。
簡単にAPIサーバーを構築できるので、ご紹介したいと思います。

対象読者
  • Go言語のWebフレームワークのechoついて知りたい!
  • Go言語でAPIサーバーを構築したい!
この記事でやること
  • Go言語のWebフレームワークのechoついてのまとめ

はじめに

echoとは、高パフォーマンス、拡張可能、軽量なWebアプリケーションフレームワークで下記のような特徴を持っています。
フレームワークとはシステム開発を簡単に行えるように汎用的な機能群がまとめられ、システムの骨組みとなるテンプレートが用意された枠組みのことになります。これを利用することで効率的にシステム開発ができるようになります。

  • 小規模から中規模Webアプリケーション向け
  • シンプルな設計により負荷がかかりづらいため、高性能
  • 堅牢でスケーラブルなRESTful APIを構築でき、拡張性がある
  • HTTP/2をサポートし、通信速度が速い
  • 最適化されたHTTPルーティングで、動的メモリの割り当てがなく、経路の優先順位をスマートに決定できる
  • 多数の組み込みのミドルウェアを利用できたり、独自のミドルウェアを定義することができる
  • JSON、XML、form-dataなど、HTTPリクエストのペイロード(※)をバインディングできる
  • JSON、XML、HTML、FILE、Attachment、Inline Stream、Blobなど、様々なデータをHTTPレスポンスとして送信できる
  • 自動的にLet’s EncriptからTLS証明書をインストールする
  • 任意のテンプレートエンジンを使ったテンプレートレンダリングができる
  • HTTPエラーハンドリングができる

※ネットワーク経由でデータ通信を行う際、やり取りするデータをある長さ毎に区切ったデータに分割(パケット)するが、このパケットには、ヘッダ部(送信元や宛先など)とデータ本体のペイロードで構成されています。

Web

Web(WWW: World-Wide-Web)とは、インターネットを利用して、Webページなどをつなぎ合わせた仕組みになります。
Webの最小単位は、Webページになり、このWebページにはURLという住所が付けられており、これらが集まったものがWebサイトになります。

インターネット

インターネットとは、様々な情報をやり取りするための通信技術になります。
世界中のコンピュータは、ネットワークとネットワークをつなぎ、相互接続したネットワーク網を用いることで通信できるようになり、異なる国の機器でもインターネットを利用することで通信できるようになります。

Webサービス

Webサービスとは、インターネットを利用したサービスのことで、Youtubeやmercari、Twitterなどがあります。
各サービスはサーバーを持っており、そのサーバーに問い合わせることで最新情報を取得し、クライアント(ブラウザ)に表示したり、世界中のユーザーと情報共有ができます。
サーバーとは、Web上で情報やデータを提供する専用のコンピューターのことで、
クライアントとは、サーバーに対して何らかのリクエストをし、サーバーから情報を受信して表示や処理を行うコンピューターになります。
サーバーとクライアントのやり取りは下記のような流れで行われます。

  1. クライアントがサーバーへ「リクエスト」を投げる
  2. サーバーがリクエストを解析、処理してリクエストのレスポンスを作る
  3. サーバーがクライアントへ「レスポンス」を返す

HTTP

HTTPとは、Web上でクライアントとサーバーが通信するためのプロトコルになります。
プロトコルとは、データをやり取りをする際の決め事になります。
Webサイトのページのファイル等を転送する際に使用し、画像や動画、音声などコンピューター上で扱えるデータであれば転送することができます。
クライアントとサーバーをこのHTTPプロトコルに則りデータのやり取りを行います。

HTTPリクエスト

HTTPリクエストの中身は下記のようになっています。

メソッド URI(リクエストの対象) ブラウザがサポートしているHTTPのバージョン
ヘッダ部(付加情報)
 (空行)
リクエストボディ部
メソッド

メソッドはリクエストの対象リソース(データ、ファイルなど)に対して、どういった操作を行うかを指定し、下記のような種類があります。

メソッド説明冪等性 安全性CRUD
GETリソースの取得READ
POSTリソースの新規作成××CREATE
PUTリソースの新規作成・更新×UPDATE
DELETEリソースの削除×DELETE
PATCHリソースの部分更新×
HEADリソースのヘッダを取得
OPTIONSリソースがサポートしているメソッドの取得
TRACE自分宛てにリクエストメッセージを返す試験
CONNECTプロキシ動作のトンネル接続への変更

POSTPUTもリソースの新規作成を行いますが、PUTの場合は、更新もできます。POSTPUTは、HTTPリクエストの渡し方に違いがあります。
どちらもHTTPリクエスト内のボディに追加または更新するリソースのデータを入れますが、PUTの場合は、URIの中でIDなどを指定し、あるリソースに対して直接指定することができます。
例えば、
POST: /api/users
PUT: /api/users/{id}

また、OPTIONSメソッドは、GETPOSTなどの実際のリクエストを送る前に、送っても問題ないかを確認するためのプリフライトリクエストを送るために使用されます。

冪等性

何度実行しても同じ結果になる性質のことです。
例えば、DELETEメソッドを使用して、あるリソースを削除した後に同じリクエストを実行した場合、リソースは削除されたままになります。

安全性

任意のリソースを読み取るためのリクエストでサーバーの状態を変更するものではない性質のことです。

冪等性と安全性により、クライアントは信頼できないネットワーク経由で、信頼できるHTTPリクエストを送信することができます。

URI

URI(Uniform Resource Identifier)は、リソースの名前(URN: Uniform Resource Name)とアドレス(Uniform Resource Locator)であり、Web上の情報、データを一意に特定するための仕組みで、このURIを通じて提供されます。
URIには、スキーム、ホスト名、ポート番号、パス名、クエリパラメータ、パスパラメータ、URIフラグメントを付けることができます。 


リソースは少なくとも1つのURIを持っていなければなりません。例えば、あるURIをブラウザに入力するとブラウザ上に情報が表示されるようにリソースはURIを持っていなければなりません。

URIが扱うリソースは予測可能であるべき

URIとリソースは、直観的にわかりやすく対応させるのが好ましいです。
例えば、ユーザーデータを検索する際/i-wanna-know-about/usersではなく、/search/usersとした方がこのURIはどういったリソースを扱うのかが予測可能でわかりやすくなります。

パスパラメータ

特定のリソースを識別するために必要な情報です。

クエリパラメータ

特定のリソースを操作して取得する際に使用する情報です。
検索やフィルタリングなどに関する条件で使用します。

ヘッダ

リクエストに下記のような追加情報を入れます。

追加情報説明
Hostドメイン名
Referer直前に参照したURL
Cookieブラウザに保存されているCookieデータ
User-Agentリクエストしたクライアントのアプリケーションのタイプ
アクセス元の機種とブラウザ
Accept、
Accept-Language、
Accept-Encoding、
Accept-Charset
クライアントが受け付ける
データ
言語
リソースの圧縮手法
文字コード
リクエストボディ

リソースの新規作成や更新を行う際に使用するデータを入れます。
データの形式は、JSONやXMLなど様々指定することができます。

{
  "users": [
    {
      "user_name": "user1",
      "age": 10
    },
    {
      "user_name": "user2",
      "age": 20
    }
  ]
}

HTTPレスポンス

HTTPレスポンスの中身は下記のようになっています。

HTTPのバージョン ステータスコード 理由
ヘッダ部(付加情報)
 (空行)
ボディ部
ステータスコード

ステータスコードは、HTTPリクエストに対するサーバーの結果を表します。また、ステータスコードには下記のような種類があります。

番台分類ステータスコード説明
1XX情報処理中
2XX成功200OK
リクエスト成功
201Created
リクエスト成功し、データ作成成功
204No Content
リクエストは成功し、特に返すデータがない
3XXリダイレクト301Moved Parmanently
URIが変更された
4XXクライアントエラー400Bad Request
不正なリクエストエラー
401Unauthorized
認証エラー
403Forbidden
アクセス権限エラー
404Not Found
リソースが見つからないエラー
407Proxy Authentication Required
プロキシサーバー経由で通信を行う際にプロキシサーバーの認証情報が不足しているエラー
408Request Timeout
リクエスト送信後のやり取りに時間が長すぎるためタイムアウトエラー
409Conflict
サーバーに既に存在しているデータが競合しているためリクエストを完了できない
5XXサーバーエラー500Internal Server Error
何らかのサーバー内部のエラー
502Bad Gateway
ゲートウェイが不正なリクエストを受け取り拒否したエラー
503Service Unabailable
一時的にサーバーにアクセスできないエラー
504Gateway Timeout
リクエストを送ったサーバからの適切なレスポンスがなくタイムアウトエラー
ヘッダ

レスポンスに下記のような追加情報を入れます。

追加情報説明
Dateレスポンス生成日
Serverサーバーの名前とバージョン
Content-Typeリソースのデータ形式
ConnectionKeep-Alive
一度使用したコネクションを継続して利用でき、次回以降のコネクション接続コストを減らすことができる
Keep-Aliveタイムアウトや最大接続数の設定内容
レスポンスボディ

クライアントに返すHTMLや画像などのデータが入ります。

RESTful API

RESTful APIとは、RESTの原則に則って構築されたWebシステムのHTTPでの呼び出しインターフェースのことになります。
ここでのインターフェースは、あるシステムと別のシステムが相互通信できるようにした通信仕様になります。

REST

REST(REpresentational State Transfer)とは、分散型システムにおける複数のソフトウェアを連携されるのに適した設計原則、考え方のことになります。
RESTには、4つの原則があり、これをリソース指向アーキテクチャ(ROA: Resource Oriented Architecture)と呼び、RESTful APIは、このROAを実装しているということになります。

  1. アドレス可能性(Addressability)
    • 提供する情報がURIを通して表現できること
    • 全ての情報はURIで表現される一意なアドレスを持つこと
  2. ステートレス性(Stateless)
    • HTTPをベースにしたステートレスなクライアント/サーバープロトコルであること
    • セッション等の状態管理はせず、やりとりされる情報はそれ自体で完結して解釈できること
  3. 接続性(Connectability)
    • 情報の内部に、別の情報や状態へのリンクを含めることができること
  4. 統一インターフェース(Uniform Interface)
    • 情報の操作(取得、作成、更新、削除等)は全て、HTTPメソッド(GET、POST、PUT、DELETE)を利用すること

リソース指向アーキテクチャ(ROA)

まず、リソースとはWeb上に存在する情報、データのことで、コンピュータ上に格納することができ、一連のビットで表現できます。
リソース指向アーキテクチャは、このリソースを中心に考えるアーキテクチャのことになります。リソースという概念、リソースはURIを持つ、リソースに対する操作、リソースの表現形式というようにリソース指向の中心は、リソースになります。

リソースの表現

Webサーバーはリソースを特定の言語(英語、日本語、・・・)、フォーマット(XML、CSV、・・・)で送信しなければなりません。
サーバーがリソースの表現を複数提供する場合、各表現に対して、個別のURIを割り当てます。例えば、/resource/en/resource/jaといったように別々のURIを割り当てます。このURIもサーバーがリクエストを処理するために必要な情報が全てURIに含まれているということになります。ただし、この方法は、同じリソースなのにURIだけを見ると別のリソースを指しているようにも見える欠点があるため、コンテンツネゴシエーション(今回は説明しない)という方法を使用することで、同じでURIで様々な言語やフォーマットのリソースを提供することができるようになります。

アドレス可能性

アドレス可能性とは、URIを通じてリソースを簡単に指し示す性質のことになります。
アプリケーションがデータセットの重要な部分をリソースとして公開する場合、そのアプリケーションはアドレス可能となります。リソースはURIを通じて提供されるため、アドレス可能なアプリケーションは提供可能な情報毎にURIを公開します。

ステートレス性

ステートレス性とは、全てのHTTPリクエストが完全に分離している性質のことになります。
状態には、アプリケーション状態とリソース状態があります。サーバー側でアプリケーション状態の管理は行いません。クライアントが送信するHTTPリクエストには、サーバーがそのリクエストを処理するために必要な情報が全て含まれており、以前のリクエストにも依存しなく、Cookie等でセッション管理も行いません。

アプリケーション状態

クライアント側で保持される状態のことで、サーバーでは保持しません。
何らかの検索エンジンを使用する際、検索クエリや表示されている検索結果ページはクライアントによって異なります。

リソース状態

サーバー側で保持される状態のことで、クライアントでは保持しません。
リソースは時間の経過や条件とともに変化する可能性があります。

セッション管理

クライアントとサーバー間で通信を行う際に、通信相手の特定や相手の状態の把握を行う仕組みになります。
また、通信相手を特定した状態の通信のことをセッションと呼び、セッションができると各アプリケーション間やサーバーとのデータのやり取りができるようになります。
HTTPプロトコルは、クライアントとの通信状態を保持しないステートレスなので、Webブラウザからのリクエスト内容が同じであれば、Webサーバーは毎回同じレスポンスを返すことになります。しかし、この場合、ログインしたユーザー毎に異なる処理や複数のWebページにまたがった一連の処理(トランザクション)を行うことできません。これらの処理を行うために、アクセスしてきたユーザーが誰なのかを識別し、これまでにどのような処理を行ってきたのかという「状態」を把握する必要があります。そのために用いられるのがこのセッション管理になります。
セッション管理の方法としては、Cookieを使用する方法、URIにセッションIDを記載する方法、フォームにセッションIDを埋め込む方法などがありますが、最も一般的なのが、Cookieを使用する方法になります。

Cookie

ブラウザにログインIDやパスワード、閲覧履歴などのユーザー情報が保存されている情報になります。
ブラウザからWebページにアクセス時、WebサーバーがブラウザにCookieを送り、ブラウザの記憶領域に格納しておき、次にアクセスする際にリクエストの中にCookieを埋め込んで送信することでWebサーバーがどのユーザーかを識別し、データを返してくれることになります。

接続性

接続性とは、リソースに別のリソースのリンクを含めてそのリソースへ遷移できる性質になります。
リソースには別のリソースへのリンクを含めることができ、別のリソースに接続することできます。

統一インターフェース

統一インターフェースとは、リソースの操作をする際に、全てHTTPメソッドが定義通りに使われることになります。
リソースを取得する場合、URIを/getResourceのようにURIの中で操作を表す表現を使用せず、/resouceのようにしてHTTPのGETメソッドを利用することで一貫した(統一された)インターフェースを保証していることになります。
全てのサービスで同じインターフェースを使用することで、任意の複数のサービスを任意の複数のサイトのようにサポートできるようになります。

REST APIを使用するメリット

  • URIに規律が生まれることで、APIを利用するサービス開発者が楽になる
  • URIに規律が生まれることで、API開発者もURIからソースのどの部分なのかが容易にわかる
  • ブラウザのアドレスバーにURIを入力すればリソースが参照できる
  • サーバー、クライアント間で何も共有しないことにより、負荷に応じたスケーラビリティが向上する
  • GET、POST、PUT、DELETE等のHTTPメソッドを使用することで、シンプルで一貫性のあるリクエスト標準化が行える

echoの使い方

インストール

下記のコマンドでインストールできます。

// go install github.com/labstack/echo/{version}
go install github.com/labstack/echo/v4

ただし、開発の一番最初は上記のコマンドのみでは、インストールすることができません。
Go言語を開発する中で外部モジュールを使用する場合、依存モジュール管理ツールのGo Modulesを使用する必要があります。
Go Modulesの使用法については、こちらの記事を参考にしていただければと思います。

echoサーバー構築

goの実行環境は、こちらの記事で作成したDockerで構築しています。
echoサーバーは、echoインスタンス作成後、そのインスタンスにルーティングを設定し、サーバーを起動することで構築できます。
echoサーバーを起動し、http://locakhost:80に対してリクエストすると、Hello, World!の文字列が返ってきます。
ルーティング設定で登録されたハンドラ(何かの処理リクエストが発生した時に起動するもの)は、設定したパスとHTTPメソッドのリクエストのみ受け取るようになっているため、別のパスやメソッドのリクエストに対しては処理することはありません。
ハンドラのecho.Contextには、リクエストの内容などの情報を保持しており、このContextStringメソッドは、リクエストに対するステータスコードと文字列をレスポンスに書き込むためのメソッドになります。

// echo型
type Echo struct {
  ・・・
  middleware []MiddlewareFunc
  router *Router
  Server *http.Server
  Validator Validator
  Logger Logger
  ・・・
}

// インスタンス作成
echo.New() (e *Echo)

// 作成したインスタンスにハンドラを登録し、ルーティング設定
func (e *Echo) Get(path string, h HandlerFunc, m ...MiddlewareFunc) *Route

// サーバーを起動し、指定されたアドレスへのリクエストの待ち受けを開始
func (e *Echo) Start(address string) error

// コンテキストインターフェース
type Context
type Context interface {
  ・・・
  // ステータスコードと文字列を返す
  String(code int, s string) error
  ・・・
}
package main

import (
  "net/http"
	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
go run main.go

echoサーバーを起動するとターミナルに下記が表示されます。

curl -X GET http://localhost:80
Hello, World!

ルーティング

前述のサーバー構築で既に使用しましたが、全てのHTTPメソッドのハンドラーを登録することができ、func(echo.Context) errorを満たす関数はハンドラとして登録することができます。

// echo型のルーティング設定のメソッド
type echo
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route
func main() {
  // インスタンス作成
  e := echo.New()

  // HTTPメソッド(URL, ハンドラの関数)  
  e.POST("/users", CreateUser)
  e.GET("/users/:id", GetUser)
  e.PUT("/users/:id", UpdateUser)
  e.DELETE("/users/:id", DeleteUser)
}


func CreateUser(c echo.Context) errot {・・・}
func GetUser(c echo.Context) errot {・・・}
func UpdateUser(c echo.Context) errot {・・・}
func DeleteUser(c echo.Context) errot {・・・}

ルーティングには、HTTPメソッド、URLパターン、ハンドラを登録します。
上記のコードの中の:idは、プレースホルダとなります。
プレースホルダとは、入力値などの外部から与えられる要素を反映させるためにそのデータが入る予定の場所のことになり、クライアントのリクエスト時にパスパラメータとして指定したデータを受け取ることができます。

パスパラメータ設定と取得

ルーティング設定に:idの形式でパスを設定すると、パスパラメータとして登録することができます。パスパラメータは、echo.ContextParamメソッドを使用することで受け取ることができます。

// コンテキストインターフェース
type Context
type Context interface {
  ・・・
  func Param(name string) string
  ・・・
}
package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
  e.GET("/user/:id", handler.GetUser)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package main

import (
	"net/http"

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

// パスパラメータを受け取って返す
func GetUser(c echo.Context) error {

  // パスパラメータを受け取る
	id := c.Param("id")

	return c.String(http.StatusOK, id)
}
go run main.go
curl -X GET http://localhost:80/users/100
"100"

クエリパラメータの取得

クエリパラメータは、echo.ContextQueryParamメソッドを使用することで受け取ることができます。
また、echo.ContextJSONメソッドを使用することでGoの型をJSONにエンコードしてステータスコードと一緒にレスポンスに書き込むことができます。

// コンテキストインターフェース
type Context
type Context interface {
  ・・・
  func QueryParam(name string) string
  ・・・
}
package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
  e.GET("/users, handler.GetUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package main

import (
	"net/http"

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

// クエリパラメータを受け取って返す
func GetUsers(c echo.Context) error {

	sort := c.QueryParam("sort")
	limit := c.QueryParam("limit")

	res := map[string]interface{}{
		"sort":  sort,
		"limit": limit,
	}

	return c.JSON(http.StatusOK, res)
}
go run main.go
curl -X GET http://localhost:80/users\?sort\=true\&limit\=1000
{
  "limit":"1000",
  "sort":"true"
}

ヘッダー情報の取得

リクエストのヘッダー情報は、echo.ContextRequestメソッドを使用することで受け取ることができます。
Requestメソッドは、標準パッケージnet/httpHeader型を返し、そのHeader型のGetメソッドにヘッダー情報のキーを渡すことでヘッダー情報を取得することができます。

// コンテキストインターフェース
type Context
type Context interface {
  ・・・
  func Request() *http.Request
  ・・・
}

// net/httpパッケージのHeaderインターフェース
type Request
type Request interface {
  ・・・
  Header Header
  ・・・
}

// Header型と埋め込んだメソッド
type Header map[string][]string
func (h Header) Get(key string) string
package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
  e.PUT("/users/:id", handler.UpdateUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package main

import (
	"net/http"

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

// ヘッダーのコンテンツタイプを受け取って返す
func UpdateUsers(c echo.Context) error {
	return c.String(http.StatusOK, c.Request().Header.Get("Content-Type"))
}
go run main.go
curl -X PUT -H "Content-Type: application/json" "http://localhost:80/users/1" -d '{"name": "Osamu","age": 20}'
application/json

リクエストデータを構造体にバインド

echo.ContextBindメソッドを使用することでリクエストデータを構造体にバインドすることができます。
バインドしたい構造体のキーにバインドするデータ用のタグをつけることで実現でき、そのタグでバインドできるデータは下記になります。

バインドデータ構造体のキーのタグ
ヘッダーheader
クエリパラメータquery
パスパラメータparam
リクエストボディjson
xml

クエリパラメータのバインド

// コンテキストインターフェース
type Context
type Context interface {
  ・・・
  func Bind(i interface{}) error
  ・・・
}
package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
  e.GET("/users, handler.GetUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package request

type GetUsers struct {
	limit int  `query:"limit"`
	sort  bool `query:"sort"`
}
package main

import (
	"net/http"

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

// クエリパラメータを受け取って返す
func GetUsers(c echo.Context) error {

	u := request.GetUsers{}
	if err := c.Bind(&u); err != nil {
		err = errors.Wrap(err, "failed to bind request data.")
	}

	return c.JSON(http.StatusOK, res)
}
go run main.go
curl -X GET http://localhost:80/users\?sort\=true\&limit\=1000
{
  "limit":"1000",
  "sort":"true"
}

パスパラメータとリクエストボディのバインド

package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
	e.PUT("/users/:id", handler.UpdateUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package request

type UpdateUsers struct {
	ID int `param:"id"`
	Name string `json:"name"`
	Age int `json:"age"`
}
package handler

import (
	"net/http"

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

// リクエストボディを受け取って返す

func UpdateUsers(c echo.Context) error {

	u := request.UpdateUsers{}
	if err := c.Bind(&u); err != nil {
		err = errors.Wrap(err, "failed to bind request data.")
	}

	return c.JSON(http.StatusOK, u)
}
go run main.go
curl -X PUT -H "Content-Type: application/json" "http://localhost:80/users/1" -d '{"name": "Osamu","age": 20}'
{
  "ID":1,
  "name":"Osamu",
  "age":20
}

リクエストデータのバリデート

echo.ContextBindメソッドでバインドする構造体のタグにvalidateタグを付与することでecho.Contextvalidateメソッドで取得するデータのバリデーションを行うことができます。バリデーションには、標準に設定されているものだけでなく、独自のカスタムバリデーションを作成することができます。
標準のバリデーションは、最大値、最小値や文字数、数値のみなど多くの設定があるため使用する場合は、公式ページを確認してください。
バリデーションを適用する場合は、echoインスタンスに登録する必要があります。

go install github.com/go-playground/validator/v10
// バリデート型
type Validate struct {
	// contains filtered or unexported fields
}

// バリデートインスタンス作成
func New() *Validate

// バリデート型のカスタムバリデーションを登録するメソッド
func (v *Validate) RegisterValidation(tag string, fn Func, callValidationEvenIfNull ...bool) error

// バリデート型の構造体のバリデートをするメソッド
func (v *Validate) Struct(s interface{}) error

// カスタムバリデーションのインターフェース
type CustomTypeFunc func(field reflect.Value) interface{}

// 構造体のフィールドをバリデートするためのメソッドを持ったFieldLevel型のインターフェース
type FieldLevel
type FieldLevel interface {
  ・・・
  Field() reflect.Value
  ・・・
}
// コンテキストインターフェース
type Context
type Context interface {
  ・・・
  Validate(i interface{}) error
  ・・・
}

標準バリデーション

package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // インスタンスにバリデーションを設定
	e.Validator = handler.NewValidator()

  // ルーティング設定
  e.GET("/users, handler.GetUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package handler

import (
  "github.com/go-playground/validator/v10"
  "github.com/labstack/echo/v4"
)

// カスタムバリデーターの構造体
type CustomValidator struct {
  validator *validator.Validate
}

// カスタムバリデーションの登録とインスタンス作成
func NewValidator() echo.Validator {
  validator := validator.New()

  return &CustomValidator{validator: validator}
}

// バリデーション関数
func (cv *CustomValidator) Validate(i interface{}) error {
  return cv.validator.Struct(i)
}
package request

type GetUsers struct {
  Limit int `query:"limit" validate:"required,max=100` // バリデーション設定
  Sort bool `query:"sort" validate:"required"` // バリデーション設定
}
package response

import (
	"reflect"
)

var (
	ErrBadRequest = 1
)

type Error struct {
	Message string `json:"message"`
	Code    int    `json:"code"`
}

type ErrorResponse struct {
	Status int `json:"status"`
	Error  Error
}

func NewErrorResponse(httpStatus int, errCode int, errMeasage interface{}) ErrorResponse {
	return ErrorResponse{
		Status: httpStatus,
		Error: Error{
			Message: getString(errMeasage),
			Code:    errCode,
		},
	}
}

func getString(i interface{}) string {
	switch v := i.(type) {
	case error:
		return v.Error()
	default:
		return reflect.ValueOf(v).String()
	}
}
package handler

import (
  "net/http"

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

// クエリパラメータを受け取って返す
func GetUsers(c echo.Context) error {
  u := request.GetUsers{}
  if err := c.Bind(&u); err != nil {
    err = errors.Wrap(err, "failed to bind request data.")
    return c.JSON(http.StatusBadRequest, response.NewErrorResponse(http.StatusBadRequest, response.ErrBadRequest, err))
	}

	if err := c.Validate(u); err != nil {
    err = errors.Wrap(err, "invalid condition parameter.")
    return c.JSON(http.StatusBadRequest, response.NewErrorResponse(http.StatusBadRequest, response.ErrBadRequest, err))
	}

  return c.JSON(http.StatusOK, u)
}
go run main.go
curl -X GET http://localhost:80/users
{
  "status": 400,
  "Error": {
    "message": "invalid condition parameter.: Key: 'GetUsers.Limit' Error:Field validation for 'Limit' failed on the 'required' tag\nKey: 'GetUsers.Sort' Error:Field validation for 'Sort' failed on the 'required' tag",
    "code": 1
  }
}
curl -X GET http://localhost:80/users\?sort\=true\&limit\=1000
{
  "status":400,
  "Error":{
    "message":"invalid condition parameter.: Key: 'GetUsers.Limit' Error:Field validation for 'Limit' failed on the 'max' tag",
    "code":1
  }
}
curl -X GET http://localhost:80/users\?sort\=true\&limit\=100
{
  "Limit":100,
  "Sort":true
}

カスタムバリデーション

package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // インスタンスにバリデーションを設定
	e.Validator = handler.NewValidator()

  // ルーティング設定
  e.GET("/users, handler.GetUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package handler

import (
  "github.com/go-playground/validator/v10"
  "github.com/labstack/echo/v4"
)

// カスタムバリデーターの構造体
type CustomValidator struct {
  validator *validator.Validate
}

// カスタムバリデーションの登録とインスタンス作成
func NewValidator() echo.Validator {
  validator := validator.New()

  // カスタムバリデーションの登録
  _ = validator.RegisterValidation("hoge", IsHoge)

  return &CustomValidator{validator: validator}
}

// バリデーション関数
func (cv *CustomValidator) Validate(i interface{}) error {
  return cv.validator.Struct(i)
}

// カスタムバリデーション
func IsHoge(fl validator.FieldLevel) bool {
	return fl.Field().String() == "hoge"
}
package request

type GetUsers struct {
  Limit int  `query:"limit" validate:"required,max=100` // バリデーション設定
  Sort  bool `query:"sort" validate:"required,hoge"` // バリデーション設定
}
package response

import (
	"reflect"
)

var (
	ErrBadRequest = 1
)

type Error struct {
	Message string `json:"message"`
	Code    int    `json:"code"`
}

type ErrorResponse struct {
	Status int `json:"status"`
	Error  Error
}

func NewErrorResponse(httpStatus int, errCode int, errMeasage interface{}) ErrorResponse {
	return ErrorResponse{
		Status: httpStatus,
		Error: Error{
			Message: getString(errMeasage),
			Code:    errCode,
		},
	}
}

func getString(i interface{}) string {
	switch v := i.(type) {
	case error:
		return v.Error()
	default:
		return reflect.ValueOf(v).String()
	}
}
package handler

import (
  "net/http"

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

// クエリパラメータを受け取って返す
func GetUsers(c echo.Context) error {
  u := request.GetUsers{}
  if err := c.Bind(&u); err != nil {
    err = errors.Wrap(err, "failed to bind request data.")
    return c.JSON(http.StatusBadRequest, response.NewErrorResponse(http.StatusBadRequest, response.ErrBadRequest, err))
	}

	if err := c.Validate(u); err != nil {
    err = errors.Wrap(err, "invalid condition parameter.")
    return c.JSON(http.StatusBadRequest, response.NewErrorResponse(http.StatusBadRequest, response.ErrBadRequest, err))
	}

  return c.JSON(http.StatusOK, u)
}
go run main.go
curl -X GET http://localhost:80/users\?sort\=true\&limit\=100
{
  "status": 400,
  "Error": {
    "message": "invalid condition parameter.: Key: 'GetUsers.Sort' Error:Field validation for 'Sort' failed on the 'hoge' tag",
    "code": 1
	}
}

ミドルウェアの設定

ミドルウェアとは、HTTPリクエストがあった際にハンドラが呼ばれる前に必ず呼ばれる機能でecho.Contextへアクセスすることができ、特定の処理を実装することができます。
例えば、HTTPリクエストのヘッダー情報などを確認して、認証認可の処理を行ったり、HTTPリクエストの一意のIDを生成することができます。
ミドルウェアの登録は、そのミドルウェアをechoUseメソッドに指定することででき、登録した順番に実行されます。
また、echo.ContextSetメソッドでコンテキストにキーを指定してデータを設定、echo.ContextGetメソッドで設定したキーを指定してデータを取得することができます。

// echo型のミドルウェアを登録するメソッド
type Echo
func (e *Echo) Use(middleware ...MiddlewareFunc)

// ミドルウェアのインターフェース
type MiddlewareFunc func(next HandlerFunc) HandlerFunc
package middleware

import (
	"github.com/labstack/echo/v4"
	"github.com/rs/xid"
)

func CreateRequestID() echo.MiddlewareFunc {

	return func(next echo.HandlerFunc) echo.HandlerFunc {

    return func(c echo.Context) error {

    // HTTPリクエスト毎に一意のデータを生成
    xid := xid.New().String()

    // コンテキストに指定したキーで生成したデータをセット
    c.Set("requestID", xid)

    // 次のミドルウェアを実行
    return next(c)
    }
	}
}
package main

import (
	"github.com/github-name/repository-name/app/handler"	
	"github.com/github-name/repository-name/app/middleware"	
  "github.com/labstack/echo/v4"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ミドルウェアを登録
	e.Use(middleware.CreateRequestID())

  // ルーティング設定
  e.GET("/users, handler.GetUsers)

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}
package main

import (
	"net/http"

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

// 生成したIDを受け取って返す
func GetUsers(c echo.Context) error {
	return c.String(http.StatusOK, request_id := c.Get("requestID"))
}
go run main.go
curl -X GET http://localhost:80/users\?sort\=true\&limit\=1000
c9m7v80ba4ft2i7qft10

CORSの設定

CORS

現代のWebはドメイン名をもとにしたオリジン(origin)という概念をもとに権限とアクセス制御を行っており、同じオリジンに由来するリソースだけを制御できるSOP: Same-Origin Policy(同一生成元ポリシー)というルールがあります。
オリジンは、プロトコル(スキーム) + ドメイン + ポート番号になります。

ドメインオリジン
google.comhttps://google.com:443


CORS: Cross-Origin Resource Sharingは、SOPのルールの1つで、異なるオリジンへのHTTPリクエストが発生する際に、相手側のオリジンからの許可を得ることでアクセスを可能にするルールになります。

echoでは、middlewareCORSWithConfigメソッドを使用することで、アクセスを許可するオリジンの設定とメソッドの設定を行うことができます。
CORS設定の登録は、echoUseメソッドを使用することでCORSの設定ができます。

// echo型のミドルウェアを登録するメソッド
type Echo
func (e *Echo) Use(middleware ...MiddlewareFunc)

// CORS設定をするための構造体
type CORSConfig struct {
  ・・・
  AllowOrigins []string `yaml:"allow_origins"`
  AllowMethods []string `yaml:"allow_methods"`
  ・・・
}

// CORSの設定を登録するメソッド
func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc
package main

import (
	"github.com/github-name/repository-name/app/handler"	
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
)

func main() {
  // インスタンス作成
  e := echo.New()

  // ルーティング設定
  e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})

  // CORSの設定
  e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins: []string{"https://example.com"},
    AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
  }))

  // サーバー起動
  e.Logger.Fatal(e.Start(":80"))
}

まとめ

この記事では、Go言語のechoについてまとめました。

少しGo言語をいじってみたいと思ったら、ブラウザ上で実行できる公式チュートリアルでコードを実際に書いてみると雰囲気がつかめるかもしれません。
今後、さらにGo言語のことについて記事にしていきたいと思います。

もし「記事の内容が間違えている!」等ありましたら、Twitterまたはお問い合わせフォームからご連絡いただければと思います。

最後までご覧いただきありがとうございました。

参考サイト

RESTful APIとは何なのか

リソース指向アーキテクチャ(ROA)とは何なのか

セッション管理

labstack/echo

labstack/echo/v4

echo.labstack.com/guide

validator