はじめに
この記事は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 |
目次
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というファイルを作成して、下記コードをコピペしましょう。
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が下記のように書き換わっていたら成功です。
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を作成して以下のコードをコピペしてください。
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 CatsやHTTP 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.go
にTask
を定義しましょう。
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
を指定します。
package db
import "[module]/todo"
type DB struct {
m map[string]*todo.Task
}
func New() *DB {
return &DB{
m: make(map[string]*todo.Task),
}
}
タスクを追加するメソッドを実装しましょう。
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
を返してエラーハンドリングを行います。
今回はtask
がnil
だったとき、ID
が同じタスクを追加しようとしたときにエラーを返すようにしました。
タスクのIDで使用する予定のUUID
はUUID(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
の定義と初期化関数の実装をします。
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
という形式で新しいタスクの情報を入れてもらって、それを参照してタスクを追加していきます。
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()
を実装していきます。
func (s *Server) Start() error {
s.r.POST("/tasks", s.addTask)
// 先程はハンドリングしていませんでしたが、実はRunメソッドもエラーを返しています
return s.r.Run()
}
あともう少しです。
main
でServer
の初期化、起動をするだけです。
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
というコマンドを使って行います。
以下コマンドで以下のような文字列が表示されていたら成功です。
curl -X POST -d 'name=hoge' -v http://localhost:8080/tasks
-X
はメソッドを指定する、-d
はリクエストボディを入力する、-v
はより詳しくやり取りを表示するためのオプションです。
* 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
に取得するための新しいメソッドを実装します。
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
で登録を追加するだけで完了です!
func (s *Server) findTasks(c *gin.Context) {
tasks := s.db.FindTasks()
// 先程まではString(text)で値を返していましたがフロントエンド的に扱いやすいJSONというフォーマットで返します
// 同じAPIでデフォルトのフォーマットが違うのはよろしくないのでaddTaskも本当はJSONで返したほうがよいですね
c.JSON(200, tasks)
}
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
オプションは不要です。
curl -v http://localhost:8080/tasks
チャレンジ
これだとレスポンスのJSON
のフィールドがIsDone
やCreatedAt
のようにキャメルケースになってしまっています。
例↓
[
{
"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
にメソッドを追加します。
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
を更新します。
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()
}
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
でリクエストボディを受け取る -
name
とis_done
の情報が更新できるようにする -
name
が空の場合はエラーを返す
実装例はyuzuy/go-guide-after-progateにあるので、自分の実装と比較したい場合やどうしてもわからなかった場合は参照してください。
まとめ
本記事ではGoの環境構築からWeb APIでのHello World、簡単なWeb APIの実装まで行ってみました。
本記事を通して少しでも読者の方が成長できたら幸いです。
今後はRDBMSを使ったToDo APIの拡張の解説などを追加しようと考えています。
はじめにでも書きましたが、なにか気になるところや提案がありましたらぜひコメントやDMお願いします!
それでは良いお年を!