Go開発者への道 第5回 APIサーバーはWebフレームワークを使用しないで作ろう

API

Go初心者向けに始めた記事の第5回になります。
今回はGoでどのようにAPIを使っていくかを紹介します。
基本的にAPIの開発をする場合は、RubyはRuby on Rails、PHPはLaravelなどAPIを開発する場合、フレームワークを使用すると思いますが、自分の働いている会社ではGoのAPI開発でフレームワークは使用しません。理由を含め説明していきます。

フレームワークをしない理由

理由として、net/httpというhttpクライアントとサーバーの実装ができる標準のライブラリが優秀だからです。net/httpがあればサーバーをすぐに作れるので、あとは足りてない部分を他のライブラリを導入するのが効果的です。

httpサーバーの実装

インストール

HTTPルーターを簡単に実装できるgo-chi、リクエストを構造体に変換してくれるbindingのライブラリをインストールします。

GitHub - go-chi/chi: lightweight, idiomatic and composable router for building Go HTTP services
lightweight, idiomatic and composable router for building Go HTTP services - go-chi/chi

GitHub - mholt/binding: Reflectionless data binding for Go's net/http (not actively maintained)
Reflectionless data binding for Go's net/http (not actively maintained) - mholt/binding

go get -u github.com/go-chi/chi
go get -u github.com/mholt/binding

コード

例として、ユーザーの作成と詳細取得のAPIを作成してみました。
今回はDBに接続しないので、レスポンスの内容はリクエストの内容と固定値を返すようにしてます。

cmd/root.go

前回からの差分として、サーバーの設定と起動を追加しています。

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
	"log"
	"net/http"
	"os"

	"github.com/SND1231/go-column/router"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type Config struct {
	Type     string
	Host     string
	Port     int
	User     string
	Password string
	Name     string
}

var config Config

var rootCmd = &cobra.Command{
	Use: "go-column",
	Run: func(cmd *cobra.Command, args []string) {
		// configの中身を出力
		log.Printf("configの中身:{type: %s, host: %s, port: %d, user: %s, pass: %s, name: %s}",
			config.Type, config.Host, config.Port, config.User, config.Password, config.Name)

		// サーバーの設定
		r := router.Get()
		srv := &http.Server{
			Addr:    ":3020",
			Handler: r,
		}

		// サーバーの起動
		srv.ListenAndServe()
	},
}

func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	// 初期化処理
	// configの設定
	rootCmd.Flags().StringP("configName", "n", "default.toml", "config file name")

	// Runを実行するたびに、initConfigを呼び出す。その後に、Runの処理が動き出す。
	cobra.OnInitialize(initConfig)
}

func initConfig() {
	configName, _ := rootCmd.Flags().GetString("configName")
	viper.SetConfigFile(configName)

	// 設定ファイルを読み込む
	if err := viper.ReadInConfig(); err != nil {
		log.Println(err)
		os.Exit(1)
	}

	// 設定ファイルの内容を構造体に設定
	if err := viper.Unmarshal(&config); err != nil {
		log.Println(err)
		os.Exit(1)
	}
}

router/router.go

ここではAPIを実装しているhandlerの初期化をして、どのURLが来た時に、どの処理を呼び出すかルーティングをしている。

package router

import (
	"github.com/SND1231/go-column/handler"
	"github.com/go-chi/chi"
)

func Get() *chi.Mux {
	r := chi.NewRouter()

	userHandler := handler.NewUserHandler()
	r.Route("/user", func(r chi.Router) {
		r.Post("/add", userHandler.Add)
		r.Get("/detail", userHandler.Get)
	})
	return r
}

handler/user.go

ユーザーの作成、詳細取得のAPIの実装はここでしている。
処理として、リクエストからリクエスト用の構造体に変換して、その値を使ってレスポンス用の構造体を作成してレスポンスを返すようにしている。

package handler

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/mholt/binding"
)

type UserHandler struct{}

func NewUserHandler() *UserHandler {
	return &UserHandler{}
}

// 失敗時のレスポンスの設定
func setErrorResponse(w http.ResponseWriter, status int) {
	// レスポンスのヘッダ設定
	w.Header().Set("Content-Type", "application/json; charset=utf-8")

	// 引数のステータス設定
	w.WriteHeader(status)
}

// 成功時のレスポンスの設定
func setSuccessResponse(w http.ResponseWriter, res []byte) {
	// レスポンスの内容をjsonに変換
	w.Write(res)
}

// ユーザーの作成APIのリクエスト
type AddUserInput struct {
	Name string
	Age  int
}

// リクエストのマッピング。ポインターレシーバーにすること
func (input *AddUserInput) FieldMap(r *http.Request) binding.FieldMap {
	return binding.FieldMap{
		&input.Name: "name",
		&input.Age:  "age",
	}
}

// ユーザーの作成APIのレスポンス
type AddUserOutput struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// ユーザーの作成API
func (h *UserHandler) Add(w http.ResponseWriter, r *http.Request) {
	var err error
	var berr binding.Errors
	var response *AddUserOutput
	var res []byte

	// request -> AddUserInput型に変換
	var request AddUserInput
	berr = binding.Bind(r, &request)
	if berr != nil {
		log.Println(berr)
		setErrorResponse(w, http.StatusInternalServerError)
		return
	}

	// responseの作成
	response = &AddUserOutput{
		ID:   1,
		Name: request.Name,
		Age:  request.Age,
	}

	// レスポンスをjsonに変換
	res, err = json.Marshal(response)
	if err != nil {
		log.Println(err)
		setErrorResponse(w, http.StatusInternalServerError)
		return
	}
	setSuccessResponse(w, res)
}

// ユーザーの詳細取得APIのリクエスト
type GetUserInput struct {
	ID int
}

// リクエストのマッピング。ポインターレシーバーにすること
func (input *GetUserInput) FieldMap(r *http.Request) binding.FieldMap {
	return binding.FieldMap{
		&input.ID: "id",
	}
}

// ユーザーの詳細取得APIのレスポンス
type GetUserOutput struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// ユーザーの詳細取得API
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
	var err error
	var berr binding.Errors
	var response *GetUserOutput
	var res []byte

	// request -> GetUserInput型に変換
	var request GetUserInput
	berr = binding.Bind(r, &request)
	if berr != nil {
		log.Println(berr)
		setErrorResponse(w, http.StatusInternalServerError)
		return
	}

	// responseの作成
	response = &GetUserOutput{
		ID:   request.ID,
		Name: "Jony",
		Age:  45,
	}

	// レスポンスをjsonに変換
	res, err = json.Marshal(response)
	if err != nil {
		log.Println(err)
		setErrorResponse(w, http.StatusInternalServerError)
		return
	}
	setSuccessResponse(w, res)
}

最終的には以下のようなリポジトリになります。

GitHub - SND1231/go-column at go-column-5
Go コラムのサンプルコードをこちらに残す. Contribute to SND1231/go-column development by creating an account on GitHub.

実行結果

今回APIの実行確認でVScodeのREST Client拡張を使用する。
VScodeの拡張機能から以下のREST Client(以下の写真)をインストールする。

使い方は以下を参照

VS Code上でHTTPリクエストを送信し、VS Code上でレスポンスを確認できる「REST Client」拡張の紹介 - Qiita
概要Visual Studio Code(以下VS Code)の拡張機能であるREST Clientが便利だったのでその紹介です。使い方を文字とgifで説明していきます。説明はマーケットプレー…

POST http://localhost:3020/user/addの実行結果

HTTP/1.1 200 OK
Date: Wed, 31 May 2023 14:07:53 GMT
Content-Length: 31
Content-Type: text/plain; charset=utf-8
Connection: close

{
  "id": 1,
  "name": "taro",
  "age": 18
}

GET http://localhost:3020/user/detail?id=1500の実行結果

HTTP/1.1 200 OK
Date: Wed, 31 May 2023 14:07:37 GMT
Content-Length: 34
Content-Type: text/plain; charset=utf-8
Connection: close

{
  "id": 1500,
  "name": "Jony",
  "age": 45
}

まとめ

今回はAPIの作成について説明をしてきました。
次はDBと接続する方法について解説をしていきたいと思います。
今回の記事以外にもGoについて書いた記事がございますので、もし興味がありましたら、おすすめ記事も見てもらえたらと思います。

【おすすめ記事のリンク】

コメント

タイトルとURLをコピーしました

Fatal error: Uncaught JSMin_UnterminatedStringException: JSMin: Unterminated String at byte 788: "RailsやPythonでWebサービスを作成する場合、Webフレームワークを使用して開発をすると思います。 in /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php:214 Stack trace: #0 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php(152): JSMin->action(1) #1 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php(86): JSMin->min() #2 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/ao-minify-html.php(257): JSMin::minify('{"@context":"ht...') #3 [internal function]: AO_Minify_HTML->_removeScriptCB(Array) #4 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/ao-minify-html.php(108): preg_replace_callback('/(\\s*)(<s in /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php on line 214