LoginSignup
15
13

More than 3 years have passed since last update.

Progate Goコースを終えた人へ贈るWeb API開発入門

Last updated at Posted at 2020-12-23

はじめに

この記事はGo 3 Advent Calendar 2020の23日目の記事です。

あなたは誰?

プログラミング歴1年半くらいで、普段はWebプログラマ(Go, React)のインターンとして働いています。

なぜこのような記事を書くに至ったか

この記事はProgateのGoコースを終えたけどこれからどうやって学んでいけばいいかわからない人のためのガイド的なものです。

筆者自身、完全プログラミング初心者のときProgateにお世話になったのですが、Goのすべてのコースを終えたあとどのように学んでいけばいいのかわからずに困惑した記憶があります。

Progateの言語紹介文句には「Googleが開発した 人気上昇中のサーバーサイド言語」とありますが、Progateのレッスンは基本的な文法を学ぶものであり、WebアプリやWeb APIを作成するガイドはありません。
巷で「やるといいよ!」と言われているA Tour of Goは難しくて手を出しにくいし、これも主に文法を学ぶものであり問題の解決にはなっていません。

Goを初心者に布教したいと思っている身として、無料かつ日本語で初心者がWeb APIの作り方を学べるガイドがないのはよくないと思ったのでこの記事を書くに至りました。

どんどん加筆修正していく予定ですので、有識者の方で追加してほしい章の提案などありましたらコメントやTwitterでお願いします!

注: わかりやすくするために厳密さに欠けるところが多々あるかと思いますがご了承ください。

この記事の対象読者

題名にもある通りProgateのGoコースを終えてちょっとだけGoの文法勉強した人(プログラミング初心者)を対象としています。

具体的には

  • Progate Go IVまでの内容を理解している + export,map,struct,methodを理解している
  • CLIでの基本的なコマンドを理解している(ProgateのCommand lineコースを終えていれば大丈夫です)

ことを前提としています。

Progateを終えてすぐに来た、あるいはProgateをやっていない人は下記記事を読んで理解していただければ大丈夫だと思います。

筆者はProgateのGoコースをやってからかなり時間が経っているため、Progate内での学習内容を把握しきれていません。
上記条件を満たしている方で理解できないところがある場合は、章の提案と同じくコメントやTwitterでご指摘いただければ幸いです!

筆者の環境

OS macOS Catalina 10.15.7
Go go1.15.4 darwin/amd64
Homebrew 2.6.1

目次

  1. Goの環境構築
  2. Hello Web API
  3. ToDo APIの開発

Goの環境構築

手元にMacしかないのでMacでの環境構築方法しか載せません。
(下手に間違った、あるいは古い情報を伝えてしまう恐れがあるので)
(有識者の方いましたらLinux/Windowsでの環境構築の編集リクエストやおすすめ記事の共有ぜひよろしくお願いします!)

Goのダウンロード・インストール

色々な種類があるのですが、本記事では手軽さを重視してHomebrewを使用します。

Homebrew自体のインストールに関しては公式サイトを参照してください。

たった一行のコマンドで完了します。

brew install go

諸々の処理が終わったらgo versionというコマンドを実行してみてください。

go version go1.15.4 darwin/amd64

のように表示されたら完了です。

GOPATH・環境変数の設定

なぜ必要なのかなどの説明は一旦省きますが、気になる方はGOPATH に(可能な限り)依存しない Go 開発環境(Go 1.15 版)という記事を読むとよいかと思います。

必要なディレクトリを作成します。下記コマンドを実行するだけでOKです。

mkdir -p $HOME/go/src $HOME/go/pkg $HOME/go/bin

shell起動時にGoに必要な環境変数を設定するようにします。
echo $SHELLというコマンドを実行して、末尾がbashであれば$HOME/.bash_profileに、zshであれば$HOME/.zshrcに下記テキストをコピペしてください。

ファイルがなければ作成してください。

export GOPATH="$HOME/go"
export GOBIN="$GOPATH/bin"
export GO111MODULE=auto

Hello World in your local environment

環境構築が完了したので早速動作確認しましょう!

まず適当にディレクトリを作ります。

mkdir $HOME/Desktop/first-go

そのディレクトリに移動したらGo Moduleを初期化します。
Go Moduleについても今は作法だと思って先に進んでもらって問題ないです。

Goのプロジェクトのパッケージやそのバージョン管理に使われるものです。

go mod init [module]

[module]にはそのプロジェクトのリポジトリのURLを設定するのが慣例なので、GitHubのアカウントがある人はgo mod init github.com/[username]/first-goとするとよいでしょう。

ない場合はgo mod init first-goでも問題ありません!

今後出てくる[module]は自分の環境のものを参照してください。

go.modというファイルが作成されていれば成功です。

そして次にmain.goというファイルを作成して、下記コードをコピペしましょう。

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

コピペできたらgo run main.goというコマンドでコードを実行してみましょう。
go runはmainパッケージmain関数があるファイルを指定するとコンパイル・実行を行ってくれるコマンドです。

Hello World!

と表示されれば環境構築は完了です!

Hello Web API

まずはディレクトリの作成、Go Moduleの初期化から行っていきます。

mkdir $HOME/Desktop/first-web-api
cd $HOME/Desktop/first-web-api
go mod init [module]

外部パッケージのダウンロード

初期化ができたら今回使う外部パッケージのダウンロードを行います。
標準パッケージでも実装できるはできるのですが、本記事ではついでに外部パッケージの使い方を学んでしまおう、ということでGinというフレームワークを使います。
さらに、タスクのIDにUUIDというものを使うのでそれを生成するためのパッケージもダウンロードします。

下記コマンドを実行しましょう。

go get github.com/gin-gonic/gin
go get github.com/google/uui

Goでの外部パッケージのダウンロードはgo getコマンドによって行います。

go.sumというファイルが生成されて、go.modが下記のように書き換わっていたら成功です。

go.mod
module [module]

go 1.15

require (
    github.com/gin-gonic/gin v1.6.3 // indirect
    github.com/google/uuid v1.1.2 // indirect
)

とりあえず何も考えずにWeb APIでHello Worldしてみる

脳死で写経→動かす→解説という流れで書いていきます。
main.goを作成して以下のコードをコピペしてください。

main.go
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello World!")
    })

    r.Run()
}

go run main.goで実行したら、任意のブラウザでlocalhost:8080/helloにアクセスしてみてください。

Hello World!と表示されていたら成功です!

確認できたらCtrl+Cを押して一旦プログラムを停止させましょう。

解説

最初の

r := gin.Default()

ではRouterの初期化を行っています。
Routerはどこにアクセスしたらどういう処理を行うか(ハンドラー)を登録するものと思ってもらえれば大丈夫です。

次の

r.GET("/hello", func(c *gin.Context) {
    c.String(200, "Hello World!")
})

ではRouterに対してハンドラーの登録を行っています。
"/hello"GETメソッドでリクエストが来たら、ステータスコード200"Hello World"という文字列を返すという処理を記述しています。

一つ目の引数には"/hello"のような引数を、二つ目の引数にはfunc(c *gin.Context)のような引数が(c *gin.Context)な関数を入れます。

急によくわからない単語が複数出てきて、読者の皆さんの頭の中には少年マガジンよろしく「!?」が浮かんでいると思うので、それらについて解説していきます。

HTTP

GETメソッドやステータスコードなど、これらはHTTPの用語です。

HTTPとは通信プロトコルの一種でMDN Web Docsの小難しい説明によると

Hypertext Transfer Protocol (HTTP) は HTML などのハイパーメディア文書を転送するためのアプリケーション層プロトコルです。このプロトコルはウェブブラウザー(クライアント)とウェブサーバー間の通信を目的として設計されていますが、他の用途でも使用されることがあります。 HTTP は旧来のクライアント・サーバーモデルに則っており、クライアントはサーバーにリクエストを送信するためにポートを開き、サーバー側からのレスポンスが返ってくるまで待機します。 HTTP はいわゆるステートレスプロトコルであり、つまりサーバーは二つのリクエスト間で何もデータを保持しません。 HTTP は多くの場合 TCP/IP 層上の通信で使用されますが、任意の信頼性があるトランスポート層、すなわち、 UDP のように知らぬ間にメッセージが失われるようなことがないプロトコルでも使用されることがあります。 RUDP — UDP に信頼性を追加したもの — も代替用として適合します。

ということですが、とりあえずは「ブラウザとWebサーバーでどういう風に通信するか偉い人たち(IETF)が決めたもの」という認識で大丈夫です。

この世に存在しているWebサーバーの多くはHTTPサーバーであり、本記事で作成するWeb APIもHTTPサーバーです。
なのでHTTPの用語が出てきたわけです。

メソッド

それで結局GETメソッドというのはなんなんだという話ですが、HTTPメソッドの一種です。
HTTPメソッドとは、HTTPサーバーにリクエストを送信するときにそのリクエストがどういう種類なのかを伝えるためのものです。

HTTPメソッドは

  • GET
  • HEAD
  • POST
  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH

の全9種類しかありません。

GETは最も基本的なメソッドであり、リソースを取得したいときに使われるメソッドです。
例えばこのQiitaの記事にアクセスするとき、ブラウザはhttps://qiita.com/yuzuy/items/cf018bd8adaed1f55c84に対してGETメソッドでリクエストを送っています。
ブラウザで直にアドレスを入力してアクセスする際はGETメソッドが使われており、localhost:8080/helloにもGETメソッドでリクエストを送ったのでHello World!という文字列が表示されました。

ステータスコード

ステータスコードはメソッドとは逆にHTTPサーバーがHTTPクライアント(ブラウザ)に状態を伝えるためのものです。

先程のプログラムでは200を返していましたが、200はOK、つまりリクエストの成功を表すコードです。
ステータスコードは結構色々ありますが、200台は正常系、400台はクライアントが悪い系、500台はサーバーが悪い系という風にざっと覚えておけば大丈夫でしょう。

MDN Web Docsが一番正確かと思いますが、HTTP CatsHTTP Status Dogsなどの動物の画像で説明しているサイトを見てみてもわかりやすいかもしれません。

404 Not Foundとかは有名ですね。

ざっとHTTPの解説をしたところで、コードの解説に戻ろうと思います。

と言っても

r.Run()

でサーバーが起動するというだけですが。

ToDo APIの開発

Web APIでのHello Worldも終えたところで、今度は少し応用的なAPIを開発していきます。

この章では以下の機能を備えたToDoアプリ用のAPIを開発します。

  • タスク一覧取得
  • タスク追加
  • タスク編集
  • タスクの削除

タスク追加

タスクの追加ができずにタスク一覧取得APIを開発しても無が帰ってくるだけなので、まず最初はタスクの追加を実装します。

先程のプロジェクトを流用してもいいですし、分けたい方は新しく作ってもいいです。

まずはタスクの構造を決めます。
main.goがあるディレクトリの下に新しくtodoというディレクトリを作って、その下にtask.goというファイルを作成しましょう。

今回タスクは

  • ID
  • 名前
  • 完了済みか
  • 作成日時

の要素を持つことにします。

todo.goTaskを定義しましょう。

todo/task.go
import "time"

type Task struct {
    ID        string
    Name      string
    IsDone    bool
    CreatedAt time.Time
}

time.Timeは標準パッケージtimeに定義されている時間を表す型です。

Taskの定義が終わりましたが、それを保存しておく場所がありません。
なので次はDBを定義しましょう。
初期化関数も一緒に定義します。

dbというディレクトリを作り、db.goというファイルを作成しましょう。
今回はidをキーにした連想配列で管理します。

todoパッケージをインポートするには[module]/todoを指定します。

db/db.go
package db

import "[module]/todo"

type DB struct {
    m map[string]*todo.Task
}

func New() *DB {
    return &DB{
        m: make(map[string]*todo.Task),
    }
}

タスクを追加するメソッドを実装しましょう。

db/db.go
package db

import (
    "errors"

    "[module]/todo"
)

type DB struct {
    m map[string]*todo.Task
}

func New() *DB {
    return &DB{
        m: make(map[string]*todo.Task),
    }
}

func (db *DB) func AddTask(task *todo.Task) error {
    // nilチェック
    if task == nil {
        return errors.New("this task is nil")
    }

    // 同じIDのタスクは追加できないようにする
    _, ok := db.m[task.ID]
    if ok {
        return errors.New("this task already added")
    }

    db.m[task.ID] = task

    return nil
}

Goでは関数の戻り値でerrorを返してエラーハンドリングを行います。
今回はtasknilだったとき、IDが同じタスクを追加しようとしたときにエラーを返すようにしました。
タスクのIDで使用する予定のUUIDUUID(v4) がぶつかる可能性を考えなくていい理由によると被る期待値は230京回らしいですが、この前Twitterで被った人がいたので(誰のTweetかは忘れちゃいましたが)、ここはマーフィーの法則に従っていくということで。

呼び出すときは

err := db.AddTask(nil)
if err != nil {
    log.Println(err)
    return
}

のように処理します。

基本的に失敗する可能性のある全ての関数はerrorを返すようにしましょう。

今回はServerという構造体に*gin.Engine(Router)と*db.DBを持たせて、Serverのメソッドでハンドラーを実装、Server.Start()でRouterにハンドラーの登録し、起動を行うようにします。

これは言葉で説明するよりもコードを見た方がわかりやすいと思うので、あまり理解できなくても少し先に進むことをおすすめします。

まずserverというディレクトリとその下にserver.goというファイルを作成してください。
Serverの定義と初期化関数の実装をします。

server/server.go
package server

import (
    "[module]/db"

    "github.com/gin-gonic/gin"
)

type Server struct {
    r  *gin.Engine
    db *db.DB
}

func New() *Server {
    return &Server{
        r:  gin.Default(),
        db: db.New(),
    }
}

Ginのハンドラーは(*gin.Context)が引数の関数、メソッドであればよいのでServer.AddTasks(c *gin.Context)というメソッドを定義して、タスクを追加するエンドポイントに対するハンドラーとします。

タスク追加のリクエストはPOSTで受け取るようにします。
POSTはリソースの作成を行うメソッドです。
POSTではリクエストボディというところデータを入れることができるので、そこにapplication/x-www-form-urlencodedという形式で新しいタスクの情報を入れてもらって、それを参照してタスクを追加していきます。

server/server.go
import (
    // 既存のimportに追加してください
    "time"

    "github.com/google/uuid"
)

func (s *Server) addTask(c *gin.Context) {
    // application/x-www-form-urlencodedでリクエストを受け取る場合はc.PostFormというメソッドで値を取り出せます
    name := c.PostForm("name")
    // nameが空だった場合
    if name == "" {
        name = "untitled"
    }
    task := &todo.Task{
        ID:        uuid.New().String(), // uuidの生成
        Name:      name,
        CreatedAt: time.Now(), // 現在時刻取得
    }

    err := s.db.AddTask(task)
    if err != nil {
        // err.Error()でエラーメッセージが取り出せます
        // 本記事ではエラーはすべてステータスコード500を返します
        c.String(500, err.Error())
        return
    }

    // 201はCreated、作成されたことを表すステータスコードです
    c.String(201, "Created")
}

ハンドラーの実装が終わったので、ハンドラーの登録、サーバーの起動を行うServer.Start()を実装していきます。

server/server.go
func (s *Server) Start() error {
    s.r.POST("/tasks", s.addTask)

    // 先程はハンドリングしていませんでしたが、実はRunメソッドもエラーを返しています
    return s.r.Run()
}

あともう少しです。
mainServerの初期化、起動をするだけです。

main.go
pakcage main

import (
    "log"

    "[module]/server"
)

func main() {
    s := server.New()
    err := s.Start()
    if err != nil {
        log.Println(err)
    }
}

これでgo run main.goを実行すれば立ち上がるはずです!
少し長かったかと思いますが、他のAPIを実装するためのベースを築くことができたので次からの機能追加は楽になりました!

ブラウザでPOSTリクエストを送るのは少し面倒なので、動作テストはcURLというコマンドを使って行います。

以下コマンドで以下のような文字列が表示されていたら成功です。

command
curl -X POST -d 'name=hoge' -v http://localhost:8080/tasks

-Xはメソッドを指定する、-dはリクエストボディを入力する、-vはより詳しくやり取りを表示するためのオプションです。

result
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /tasks HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 5
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 5 out of 5 bytes
< HTTP/1.1 201 Created
< Content-Type: text/plain; charset=utf-8
< Date: Wed, 23 Dec 2020 13:44:40 GMT
< Content-Length: 7
< 
* Connection #0 to host localhost left intact
Created* Closing connection 0

よく読むとPOST/tasksにリクエストしていることや、サーバーが201を返していることがわかると思います。

チャレンジ

今のままではタスク一覧が見れないため、プログラムが本当に正常に作動しているのかわかりません。
コードのどこかでログを出力して正常に作動しているのか確かめてみましょう。

タスク一覧取得

現状ではログでしかタスク一覧を見れないので、フロントエンド(Webアプリやモバイルアプリ)で見られるようにタスク一覧を取得するためのAPIを実装しましょう!

まずdb.DBに取得するための新しいメソッドを実装します。

db/db.go
func (db *DB) FindTasks() []*todo.Task {
    // tasksに入る要素の数はlen(db.m)だとわかっているので最初にその分のcapを確保しておくことで少し処理が早くなります
    tasks := make([]*todo.Task, 0, len(db.m))

    for _, v := range db.m {
        tasks = append(tasks, v)
    }

    return tasks
}

あとはServerにハンドラー用のメソッドを実装して、Server.Startで登録を追加するだけで完了です!

server/server.go
func (s *Server) findTasks(c *gin.Context) {
    tasks := s.db.FindTasks()

    // 先程まではString(text)で値を返していましたがフロントエンド的に扱いやすいJSONというフォーマットで返します
    // 同じAPIでデフォルトのフォーマットが違うのはよろしくないのでaddTaskも本当はJSONで返したほうがよいですね
    c.JSON(200, tasks)
}
server/server.go
func (s *Server) Start() error {
    s.r.GET("/tasks", s.findTasks)
    s.r.POST("/tasks", s.addTask)

    return s.r.Run()
}

これでサーバーを再起動すればタスク一覧APIが利用できるはずです!

cURLを使って確認してみましょう!
これはGETでリクエストを受け取るようになっているので、ブラウザでも確認できます。

cURLで確認する場合はデフォルトでGETを使うようになっているので-Xオプションは不要です。

command
curl -v http://localhost:8080/tasks

チャレンジ

これだとレスポンスのJSONのフィールドがIsDoneCreatedAtのようにキャメルケースになってしまっています。
例↓

[
  {
    "ID": "43176919-b8d6-4811-a78c-67f94a79be4e",
    "Name": "untitiled",
    "IsDone": false,
    "CreatedAt": "2020-12-23T23:27:38.239276+09:00"
  }
]

JSONのフィールドは全て小文字のスネークケースにするのが慣例なので(今まで見てきた殆どのAPIのレスポンスではスネークケースでした)下記のようなレスポンスを返せるようにしてみましょう。

[
  {
    "id": "43176919-b8d6-4811-a78c-67f94a79be4e",
    "name": "untitiled",
    "is_done": false,
    "created_at": "2020-12-23T23:27:38.239276+09:00"
  }
]

Task構造体のフィールドにjsonタグを付与することで可能です。
下記記事を参考にしてやってみましょう!
Goの構造体にメタ情報を付与するタグの基本

タスク削除

今のままだと間違ったタスクを追加してしまったときどうしようもできません。
このままだと不要なタスクが永遠に溜まり続けてしまいます。

タスク削除APIを実装してタスクを削除できるようにしましょう!

一覧取得と同様、まずはdb.DBにメソッドを追加します。

db/db.go
func (db *DB) RemoveTask(id string) error {
    // 存在しないタスクを削除しようとしたらエラーを返す
    _, ok := db.m[id]
    if !ok {
        return errors.New("this task not found")
    }

    // db.mからidがキーの要素を削除する
    delete(db.m, id)

    return nil
}

次も同様、Serverにメソッドを追加してServer.Startを更新します。

server/server.go
func (s *Server) Start() error {
    s.r.GET("/tasks", s.findTasks)
    s.r.POST("/tasks", s.addTask)
    // DELETEはリソースを削除するときに使われるメソッドです
    // エンドポイントの頭を":"にすることでその値をクライアントが決められるパラメーターになり、ハンドラーで参照できるようになります
    s.r.DELETE("/tasks/:id", s.removeTask)

    return s.r.Run()
}
server/server.go
func (s *Server) removeTask(c *gin.Context) {
    // idパラメーターの取得
    // /tasks/hoge にリクエストが来た場合はidに"hoge"が入る
    id := c.Param("id")
    err := s.db.RemoveTask(id)
    if err != nil {
        // gin.Hはmap[string]interface{}と同じです
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, gin.H{})
}

再度サーバーを再起動し、任意のタスクを削除してみましょう!

タスク編集

この機能は今まで学習してきたことを活かして自分で実装してみましょう!

以下の仕様に従って実装しましょう。

  • タスク削除APIと同じようにURLでパラメーターを受け取る
  • PATCHメソッドでリクエストを受け取る
  • application/x-www-form-urlencodedでリクエストボディを受け取る
  • nameis_doneの情報が更新できるようにする
  • nameが空の場合はエラーを返す

実装例はyuzuy/go-guide-after-progateにあるので、自分の実装と比較したい場合やどうしてもわからなかった場合は参照してください。

まとめ

本記事ではGoの環境構築からWeb APIでのHello World、簡単なWeb APIの実装まで行ってみました。
本記事を通して少しでも読者の方が成長できたら幸いです。

今後はRDBMSを使ったToDo APIの拡張の解説などを追加しようと考えています。
はじめにでも書きましたが、なにか気になるところや提案がありましたらぜひコメントやDMお願いします!

それでは良いお年を!

参考記事・引用元

15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13