100
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go速習会

Last updated at Posted at 2015-10-15

この投稿は 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

server.go
package main

import "fmt"

func main() {
	fmt.Println("hello world")
}
  • go runで、func main()が実行される
  • package mainにする。
  • ファイル名はなんでも良い
  • goでインポートできる標準パッケージ一覧: https://golang.org/pkg/

Hello server

server.go
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)
}

Hello server with servemux

server.go
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)
}

試しに、http://localhost:8080/hogeにアクセスしてみよう!

Hello negroni

$ go get github.com/codegangsta/negroni
server.go
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ディレクトリを作ったら、その構造通りに配布してくれる

public/index.html
<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.htmltest.htmlなどに変えておきましょう!
  • 詳しくはコメント参照

Hello struct

models/user.go
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 mainfunc main()ありで作っておく
  • type <名前> <型>で新しい方が作る
  • e.g. type StatusCode int
  • 今回はstruct型にUserという名前をつけたという位置づけになる

Hello object-oriented

models/user.go
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

models/user.go
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用のコメントを付けておく
server.go
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

models/user.go の抜粋
// 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

まずはCREATE DATABASEでgo_sokushuテーブルを作る

$ psql postgres
psql (9.4.4)
Type "help" for help.

postgres=> CREATE DATABASE go_sokushu;
CREATE TABLE
postgres=> 
db/db.go
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を書き換える

db/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

models/users.go
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) + ")"
}
server.go
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
  • コンフリクトした場合、使用したらエラーになるので、どっちを使うか決める変数/関数を新たに作る必要がある (厳密には、ネストの階層を見ているのでならない場合もある)
100
93
1

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
100
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?