この投稿は 2015/10/16にWantedlyで行われたGo速習会の内容です。速習会は事前知識ほぼ0を仮定して、あるテーマにおける知見を社内全体+少数の外部の参加者に広めるという公開社内勉強会です。=> 前回のSketch速習会の様子
今回は、Goで簡単なAPIサーバーを作ってみるというのをゴールにし、環境セットアップの部分はGo言語の開発環境セットアップの投稿を読んで事前にやっておいて頂きました。
当日はLive Coding形式で発表しました。各項目をCommitのDiffでみたい方は https://github.com/awakia/go_sokushu を参照ください。多少会話の流れで変わっていますが、だいたいここに書いてある内容と同じです。
この速習会でできるようになること
Go言語で簡単なAPIサーバーが書けるようになります。
- 静的ファイル配信 (APIから少し脱線するけど)
- JSON API配信
- DBアクセス
など
基本
実行の仕方
go run server.go
もしくは
go build server.go
./server
Hello world
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
- go runで、
func main()
が実行される -
package main
にする。 - ファイル名はなんでも良い
- goでインポートできる標準パッケージ一覧: https://golang.org/pkg/
Hello server
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, "hello world")
})
http.ListenAndServe(":8080", nil)
}
- https://golang.org/pkg/net/http/
- ポートはどこでもいいけど、Goは
8080
がお好きな模様
Hello server with servemux
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, "hello world")
})
http.ListenAndServe(":8080", mux)
}
- DefaultServeMuxとか所々に書いてあるはず
- https://golang.org/pkg/net/http/#ListenAndServe
- ServeMux = HTTP request multiplexer
- https://golang.org/pkg/net/http/#SurveMux
- 複数マッチする場合はマッチが長いほうが優先される
- e.g "/images/thumbnails/", "/images/", "/" という順
- つまり、"/"には全てがマッチする
試しに、http://localhost:8080/hoge
にアクセスしてみよう!
Hello negroni
$ go get github.com/codegangsta/negroni
package main
import (
"fmt"
"net/http"
"github.com/codegangsta/negroni"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, "hello world")
})
n := negroni.Classic()
n.UseHandler(mux)
n.Run(":8080")
}
- Negroni は Martini という人気を集めたライブラリの作者が、MartiniはGoぽくないとかいう批判を受け作ったミドルウェア的な立ち位置
- Rubyで言えば、MartiniがSinatra、NegroniはRackといったところか
- Martiniは、中で何をやっているかわかりづらくGoの割には遅いとか問題があった模様。Goではこういった物のほうが受け入れられやすい
- Go言語でServer作る時に必要な知識メモ にもう少し詳しくメモっておいた
Hello static assets
コードはそのまま
##negroni.Classic()
negroni.Classic() provides some default middleware that is useful for most applications:
- negroni.Recovery - Panic Recovery Middleware.
- negroni.Logging - Request/Response Logging Middleware.
- negroni.Static - Static File serving under the "public" directory.
This makes it really easy to get started with some useful features from Negroni.
つまりpublic
ディレクトリを作ったら、その構造通りに配布してくれる
<html>
<head>
<title>hello go</title>
</head>
<body>
<h1>
hoge
</h1>
</body>
</html>
-
http://localhost:8080/
でindex.html
の内容がRenderされる - 今後
HundleFunc
で設定した/
を確かめたいときは/
は何にでもマッチするので、http://localhost:8080/hoge
など適当なURLにアクセス - または、この
index.html
をtest.html
などに変えておきましょう! - 詳しくはコメント参照
Hello struct
package main
import "fmt"
type User struct {
ID int
Name string
Age int
}
func main() {
user := User{
1,
"Naoyoshi Aikawa",
29,
}
fmt.Println(user)
}
$ go run models/user.go
{1 Naoyoshi Aikawa 29}
- とりあえず、
package main
でfunc main()
ありで作っておく -
type <名前> <型>
で新しい方が作る - e.g.
type StatusCode int
- 今回はstruct型にUserという名前をつけたという位置づけになる
Hello object-oriented
package main
import (
"fmt"
"strconv"
)
type User struct {
ID int
Name string
Age int
}
func NewUser(name string, age int) *User {
return &User{
Name: name,
Age: age,
}
}
func (u *User) String() string {
return u.Name + "(" + strconv.Itoa(u.Age) + ")"
}
func main() {
user := NewUser("Naoyoshi Aikawa", 29)
fmt.Println(user)
}
go run models/user.go
Naoyoshi Aikawa(29)
いろいろ盛り込みました
- NewXxxでXxx型のコンストラクタ的なものを作るのがイディオム
-
func (自分の型) (引数) 返値
でインスタンスメソッドが作れる - こういうstructは一般的にポインタ型で返しておくと良い
Hello package
package models
import "strconv"
// User defines an user
type User struct {
ID int
Name string
Age int
}
// NewUser creates user instance
func NewUser(name string, age int) *User {
return &User{
Name: name,
Age: age,
}
}
func (u *User) String() string {
return u.Name + "(" + strconv.Itoa(u.Age) + ")"
}
- こうすることにより自分で作ったpackageを使えるようになる
- 使い方は以下
- 一応Exportする関数にgodoc用のコメントを付けておく
package main
import (
"fmt"
"net/http"
"github.com/awakia/go_sokushu/models" // 自分のパッケージ
"github.com/codegangsta/negroni"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
user := models.NewUser("Naoyoshi Aikawa", 29)
fmt.Fprintln(w, user)
})
n := negroni.Classic()
n.UseHandler(mux)
n.Run(":8080")
}
Hello JSON server
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/awakia/go_sokushu/models"
"github.com/codegangsta/negroni"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
user := models.NewUser("Naoyoshi Aikawa", 29)
userStr, err := json.Marshal(user)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, string(userStr))
})
n := negroni.Classic()
n.UseHandler(mux)
n.Run(":8080")
}
{"ID":0,"Name":"Naoyoshi Aikawa","Age":29}
という結果が見れたらOK
-
json.Marchalの定義は
func Marshal(v interface{}) ([]byte, error)
- []byte型なのでstringにキャストする
- Content-Typeを
application/json
にするのを忘れずに!
Modify JSON Response
// User defines an user
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
結果:
{"id":0,"name":"Naoyoshi Aikawa","age":29}
- structに
json:
アノテーションをするとjson.Marshalの結果が変わります -
json:"-"
で出力しない -
json:"age,omitempty"
で空(デフォルト値)だと出力しない等 - 詳細は https://golang.org/pkg/encoding/json/#Marshal
Hello unrolled/render
package main
import (
"net/http"
"github.com/awakia/go_sokushu/models"
"github.com/codegangsta/negroni"
"github.com/unrolled/render"
)
func main() {
ren := render.New()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
user := models.NewUser("Naoyoshi Aikawa", 29)
ren.JSON(w, http.StatusOK, user)
})
n := negroni.Classic()
n.UseHandler(mux)
n.Run(":8080")
}
- ダルいので簡単にRenderしてくれるライブラリを使いましょう
- go getを忘れずに!
Hello DB with OR-mapping
生SQLがGo的ですが、今回はいきなりORマッピングライブラリを使っちゃいます。
https://github.com/jinzhu/gorm
https://godoc.org/github.com/jinzhu/gorm
- gormはRails的なORマッパー
- 他はgolangでSQLを叩くライブラリまとめ[基本/クエリビルダ/ORM]がまとまっている
まずはCREATE DATABASEでgo_sokushu
テーブルを作る
$ psql postgres
psql (9.4.4)
Type "help" for help.
postgres=> CREATE DATABASE go_sokushu;
CREATE TABLE
postgres=>
package main
import (
"log"
"github.com/awakia/go_sokushu/models"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
var instance *gorm.DB
// Get gets opened db instance
func Get() *gorm.DB {
if instance != nil {
return instance
}
db, err := gorm.Open("postgres", "user=awakia dbname=go_sokushu sslmode=disable")
if err != nil {
log.Println(err)
}
instance = &db
return instance
}
func createTables() {
db := Get()
db.CreateTable(models.NewUser("Naoyoshi Aikawa", 29))
}
func main() {
createTables()
}
$ go get ./...
$ go run db/db.go
CreateTableできてるかチェック
$ psql go_sokushu
go_sokushu-# \d
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+--------
public | users | table | awakia
public | users_id_seq | sequence | awakia
(2 rows)
go_sokushu-# \d users
Table "public.users"
Column | Type | Modifiers
--------+------------------------+----------------------------------------------------
id | integer | not null default nextval('users_id_seq'::regclass)
name | character varying(255) |
age | integer |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
ここまで出来たら、一般的に使える要素だけを抜き出すようにdb.go
を書き換える
import (
"log"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
var instance *gorm.DB
// Get gets opened db instance
func Get() *gorm.DB {
if instance != nil {
return instance
}
db, err := gorm.Open("postgres", "user=awakia dbname=go_sokushu sslmode=disable")
if err != nil {
log.Println(err)
}
instance = &db
return instance
}
Hello REST
package models
import (
"fmt"
"strconv"
"github.com/awakia/go_sokushu/db"
)
// User defines an user
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
// NewUser creates user instance
func NewUser(name string, age int) *User {
user := &User{
Name: name,
Age: age,
}
db.Get().Create(user)
fmt.Println(*user)
return user
}
func GetUser(id int) *User {
var user *User
db.Get().Where("id = ?", id).First(user)
return user
}
func AllUsers() []*User {
var users []*User
db.Get().Find(&users)
return users
}
func (u *User) String() string {
return u.Name + "(" + strconv.Itoa(u.Age) + ")"
}
package main
import (
"net/http"
"github.com/awakia/go_sokushu/models"
"github.com/codegangsta/negroni"
"github.com/unrolled/render"
)
func main() {
ren := render.New()
mux := http.NewServeMux()
mux.HandleFunc("/create", func(w http.ResponseWriter, req *http.Request) {
user := models.NewUser("Naoyoshi Aikawa", 29)
ren.JSON(w, http.StatusOK, user)
})
mux.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) {
users := models.AllUsers()
ren.JSON(w, http.StatusOK, users)
})
mux.HandleFunc("/show", func(w http.ResponseWriter, req *http.Request) {
user := models.GetUser(1)
ren.JSON(w, http.StatusOK, user)
})
n := negroni.Classic()
n.UseHandler(mux)
n.Run(":8080")
}
- とりあえず、リクエストは全部Getでやってみましょう!
やってみよう!
PostやShowでIDをとったりは
でできます。
Goの注意点
- 使わない変数やパッケージがあるとエラー
-
_
に入れて切り抜けよう! - goは基本camelCaseで、constだと全部大文字みたいなルールは無い
- public(Exportするもの)は大文字始まり、privateは小文字始まり
- ライブラリがうまく見つからない時は
go get -u
それでダメならgo get ./...
- interface と struct のみ
- クラスメソッドの概念はない。インスタンスメソッドのみ
- どちらかと言うとclassでまとめるというよりpackageでまとめる
- interface は ダックタイピング
- アヒルのように歩き、アヒルのように鳴くのなら、アヒルでしょ!
- 継承はなくて埋め込み(embed)
- 考え方はモジュールに似ている
- https://golang.org/doc/effective_go.html#embedding
- コンフリクトした場合、使用したらエラーになるので、どっちを使うか決める変数/関数を新たに作る必要がある (厳密には、ネストの階層を見ているのでならない場合もある)