Instantly share code, notes, and snippets.

@ymotongpoo /gtuggirls.rst Secret
Last active Dec 22, 2015

Embed
What would you like to do?
GTUG Girls原稿

GTUG Girls Meetup #13 Go言語を使ってみよう

イベント概要

主催:GTUG Girls
日程:2013/09/12 19:00-21:45
上限参加人数:20名
講師:@ymotongpoo
チューター:@Jxck_, @tenntenn
イベントURL:https://groups.google.com/forum/#!topic/gtug-girls/fEllty6lvdM

Agenda

  • Go言語の紹介 (5分)
  • Go言語の文法紹介 (45分)
  • Webサーバを書いてみる (10分)
  • テンプレートエンジンを使ってみる (10分)
  • JSONパーサを使ってみる (10分)

Go言語の紹介

略歴

  • 2007年にRob Pike, Ken Thompson, Robert Griesemerが考案
  • 2009年に一般公開
  • 2012年にversion 1.0リリース
  • 2013年3月にversion 1.1リリース

コンセプト

  1. 単一のマシンでも巨大なコードベースをビルド
  2. 依存関係をビルドファイル無しで解決
  3. 強い型付け・柔軟・型解決も素早い型システム
  4. GCがありマルチコアの並列性をサポートする
  5. LLな書きやすさ

Go言語の文法紹介(超基礎)

おすすめサイト

独習するなら A Tour of Go をひと通りやれば習得可能。 ありがたいことに 日本語訳版 もあるので、英語が苦手な方でも 気軽に取り組めます。

またGoにはインタプリタはありませんが、標準パッケージがほとんどつかえて簡単にコードを試せる Go Playground というサイトも非常に便利です。 書いたコードに対して一意なショートカットURLも作成してくれるので、 誰かと簡単なモックアップを共有する際にも便利です。

これから紹介する文法は今回のチュートリアルを実施するために必要なものしか紹介していません。 さらに自分で何か作られる方は一度先に紹介したチュートリアルやドキュメントを実施してみてください。

Hello, World

定番のHello Worldを書いてみましょう。

package main // (1)

import "fmt" // (2)

func main() { // (3)
    fmt.Println("Hello, 世界") // (4)
}

このコードだけでも学ぶことがたくさんあります。

  1. main 関数を持つパッケージは main パッケージでなければいけません
  2. import によってパッケージをインポートします
    • パッケージ名はダブルクォーテーションでくくります。
    • 外部パッケージはレポジトリのホスト名+パス名だけ指定(例: github.com/ymotongpoo/go-twitter/twitter
  3. 実行プログラムのエントリポイントは main 関数です。
  4. パッケージ内の関数は (パッケージ名).(関数名) で指定します。

変数の宣言

Goでは変数の宣言と代入は次のように行います。

var 変数名 型名
変数名 = 値

例えばこんな具合です。

var foo int
foo = 20 // = は代入の演算子

Goは強い型付けの言語なので、型が少しでも違っていると怒られます。暗黙の型キャストと言われるような、勝手な型変換はしてくれません。たとえば次のコードはエラーになります。

var foo int
foo = 3.1415 // 3.1415はfloat64の型なので怒られる

しかし毎度毎度変数の型宣言をすると疲れるので、初期化と型宣言を楽に行う方法があります。それが := という演算子です。この演算子を使うと、代入する値を元に変数の型を推測して、変数の型を決めたあとに代入を行ってくれます。

foo := 20    // fooはintと判断される
foo = 3.1415 // fooはint型の変数として宣言されているのでfloat64の値を代入しようとするとエラーになる

この := 演算子のおかげでだいぶ楽に変数宣言ができますね。ちなみに := は変数を初期化するときにしか使えない演算子です。

関数

Goでの関数の書き方は次のような形になります。

func 関数名(引数 引数の型, 引数 引数の型...) 戻り値の型 {
   ...
}

先ほどのHello Worldプログラムをちょっとだけ書き換えてみましょう。

package main

import "fmt"

func main() {
    fmt.Println(SayHello("GTUG Girls"))
}

func SayHello(name string) string {
    return "Hello, " + name
}

if文

if文は他のプログラミング言語とほとんど同じ書き方です。

if 条件 {
    trueだった場合
} else {
    falseだった場合
}

複数の条件は論理演算子の &&|| で結合します。

for文

Goでは繰り返しの制御構文は for しかありません。 まず他の言語で一般的な for と同様の用途の場合は次のように書きます。

for 初期化式; 条件式; 再初期化式 {
    ...
}

他の言語での while のような使い方は次のような形になります。

for 条件 {
    ...
}

または無限ループは以下のように書けます。

for {
    ...
}

演習問題1

関数の書き方と iffor を学んだので、例に漏れず FizzBuzz を書いてみましょう。 文字列の表示は fmt.Println() で、余りの計算は % で行えます。

ここに解答のサンプルを載せておきます。

http://play.golang.org/p/VfOKX9NoVT

スライス

LLなどではリストやマップがありますが、Goにも同様のものがあります。それがスライスとマップです。 まずスライスは他の言語でリストと呼ばれているものと似たものです。 先ほどの := 演算子を使っての宣言だと、次のように行います。

foo := []型名{}                   // 要素0のスライスの場合
bar := []型名{要素1, 要素2, ...}  // 要素込みでのスライスの初期化

スライスの各要素はすべて同じ型でないといけません。

foo := []int{1, 2, 3, 4, 5}                   // すべてintなのでOK
bar := []string{"spam", "egg", "ham", 3.1415} // 3.1415はstringではないのでエラーになる

既存のスライスに要素を追加する場合は組み込み関数の append を使います。 上の int のスライス foo に新しい要素を追加してみます。

foo = append(foo, 10) // []int{1, 2, 3, 4, 5, 10} となった

他にも組み込み関数の make を使った初期化方法がありますので、後で確認してください。

なお for 文内では foreach のような書き方として次のように range キーワードを使うと便利です。

for i, v := range some_slice {
    ...
}

i はインデックス、 v はそのインデックスに対応する値です。

マップ

マップはPythonで言うところの辞書に当たります。

spam := make(map[キーの型名]値の型名)

たとえば、 int をキーにして、 string を値に持つマップを宣言してみましょう。

students := map[int]string{}   // intをキーとしてstringを値とするマップを作成。
students[1] = "じーたぐがーる子"
students[2] = "ぐぐるごーふぁー夫"
fmt.Println(students[1])       // "じーたぐがーる子"と表示される

こちらも make を使った初期化方法がありますので、見てみてください。 こちらも for 文内では foreach のような書き方として次のように range キーワードを使うと便利です。

for k, v := range some_map {
    ...
}

i はキー、 v はそのキーに対応する値です。

Webサーバを書いてみる

net/http パッケージ

Goの標準パッケージに net/http というHTTPサーバ&クライアント用パッケージがあります。 これを使ってHTTPサーバを書いてみましょう。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, GTUG Girls")
}

これを実行して、 http://localhost:8080/ にアクセスしてみましょう。

$ go run hello.go

今日のハンズオンではプログラムを更新して実行する際にはこの go run を使ってください。

複数のエンドポイントを用意する

先の例ではルートパスのみを受け付ける状態でしたので、複数のエンドポイントを用意してみます。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/goodbye", goodbyeHandler)
    http.ListenAndServe(":8080", nil)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, GTUG Girls")
}

func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Good Bye, GTUG Girls")
}

演習問題2

上のプログラムに対して好きなハンドラーを追加してみましょう。

HTMLを表示してみる

当然ですが文字列としてHTMLを返してあげればキチンとページが表示されますね!

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/goodbye", goodbyeHandler)
    http.ListenAndServe(":8080", nil)
}

var helloHTML = `<!doctype html>
<p style=">Hello, GTUG Girls` // (1)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, helloHTML)
}

var goodbyeHTML = `<!doctype html>
<p>Good Bye, GTUG Girls` // (1)

func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, goodbyeHTML)
}

ここで新しいことを解説します。

  1. バッククォートはPythonでいうところのトリプルクォートや多言語でのヒアドキュメントと同じ位置づけです。
    • ダブルクォートを含む文字列や複数行の文字列を扱いたいときに便利です。

テンプレートエンジンを使ってみる

テンプレートを書く

先ほどのHTMLはメッセージ部分以外は共通しているのでテンプレートを使います。 Goには標準パッケージに text/templatehtml/tempalte という2つのパッケージがあります。 後者はテンプレートからHTMLを生成する際にデータとして入力されたHTMLはエスケープをしてくれるので安心です。

GoのテンプレートはPythonのJinja2に似たシステムを持っていて、テンプレートの継承なども可能です。 詳細は http://golang.org/pkg/html/template/ を参考にしてもらうとして、 今回は変数の埋め込みをしてみます。

const BaseHTML = `<!doctype html>
<p>{{ . }}` // (1)

var BaseTemplate = template.Must(template.New("base").Parse(BaseHTML)) // (2)

このコードの解説をします。

  1. 変数は {{ }} で囲みます。渡されたデータ自身はピリオド( . )で表現されます。
    • 例えば struct を渡した場合には続けてフィールド名を書くことでアクセスできます。(eg. {{ .Name }}
  2. テンプレート用文字列は Template 型の変数の Parse() 関数で読み取って実体化します。
    • (2)の書き方がイディオムです。

これを先ほどのコードに適用すると次のようなコードとなります。

package main

import (
    "html/template"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/goodbye", goodbyeHandler)
    http.ListenAndServe(":8080", nil)
}

const BaseHTML = `<!doctype html>
<p>{{ . }}`

var BaseTemplate = template.Must(template.New("base").Parse(BaseHTML))

func helloHandler(w http.ResponseWriter, r *http.Request) {
    BaseTemplate.Execute(w, "Hello, GTUG Girls")
}

func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
    BaseTemplate.Execute(w, "Good Bye, GTUG Girls")
}

JSONパーサを使ってみる

概要

JSONは encoding/json を使って扱うことができます。 PythonなどではJSONをPythonの辞書型に変換しますが、Goではstructに変換します。

今回は OpenWeatherMap API を使ってみましょう。 試しに東京都の最新の天気を取得してみます。

$ curl "http://api.openweathermap.org/data/2.5/find?lat=35.6895&lon=139.6917&cnt=1"

次のようなJSONが返ってきます。

{
  "message":"accurate",
  "cod":"200",
  "count":1,
  "list":[
    {
      "id":1850144,
      "name":"T\u014dky\u014d-to",
      "coord":{
        "lon":139.691711,
        "lat":35.689499
      },
      "main":{
        "temp":297.74,
        "humidity":43,
        "pressure":1013,
        "temp_min":295.37,
        "temp_max":300.37
      },
      "dt":1378815731,
      "wind":{
        "speed":2.95,
        "deg":84.5
      },
      "sys":{
        "country":"JP"
      },
      "rain":{
        "1h":0.25
      },
      "clouds":{
        "all":0
      },
      "weather":[
        {
          "id":521,
          "main":"Rain",
          "description":"shower rain",
          "icon":"09n"
        }
      ]
    }
  ]
}

この中から、 weathermaindescription を取得して表示するとしましょう。

HTTPリクエストをする

まずJSONを取得するためにAPIに対してHTTPリクエストを投げます。 HTTPリクエストはサーバと同様に net/http を利用します。 典型的なリクエストは次の形で行います。

resp, err := http.Get(URL)   // (1)
if err != nil {
    log.Println(err)
    return
}
defer resp.Body.Close()      // (2)

ここでまた新しいことが出てきました。

  1. http.GET(URL)URL に対してGETリクエストを行うことができます。
  2. resp.Body はきちんと閉じるように deferClose() メソッドを呼んでおきましょう。
    • defer とは関数の最後にきちんと処理されるようにするためのキーワードです。

取得したJSONをパースする

JSONのパースのためには先ほど説明したように専用のstructが必要となります。 ここで struct について説明します。

struct

Goにはクラスがありません。その代わり struct と interface というものがあります。 後者は今回は飛ばしますが大事なので後で調べてみてください。

struct はフィールドと呼ばれる変数を束ねたものと考えてください。 例えば Human 型の struct を宣言します。

type Human struct {
    Name   string
    Age    int
    Height float64
    Weight float64
}

struct の初期化と更新は次のように行います。

taro := Human{} // 初期化
taro.Name = "Yamada Taro"
taro.Age = 20

hanako := Human{ // まとめて初期化する場合
    Name: "Yamada Hanako",
    Age: 21,
    Height: 158,
    Weight: 50, // 最後のカンマは忘れがちなので注意
}

また struct はメソッドを持つことが出来ます。メソッドの書き方は関数とほとんど一緒で次のとおりです。

func (型変数名 struct名) メソッド名(引数 引数の型, ...) 戻り値の型 {
    ...
}

たとえば上の Human 型に Greet() というメソッドを追加してみます。

func (h Human) Greet() string {
    fmt.Printf("Hello, My name is %s. %d years old.", h.Name, h.Age)
}

JSONのパース

さて、structの宣言方法と初期化の方法がわかったのでJSONをパースしてみます。 コツとしてはJSONで値に対応するものが通常のデータ型(intやstring等)でなかった場合、 新しく型を宣言して、その型のフィールドとして定義してしまうことです。

先ほどの返り値を見てみましょう。まず返ってきたJSON全体を表すstructを定義します。 weather の中身はマップになっているので、ここで Weather 型というstructを用意します。

type APIResult struct {
    L []List `json:"list"` // (1)
}

type List struct {
    W []Weather `json:"weather"` // (2)
}

type Weather struct {
    Main        string `json:"main"`  // (3)
    Description string `json:"description"`  // (3)
}

またまた新しいことが出てきました。

  1. まず weather を含む、返り値のJSONのトップレベルにある "list" というキーを取得するため、 json:"list" というラベルを定義します。
    • リストを受け取る場合は、新しい型を定義して(この場合は List )、それのスライスにします
  2. "list" の要素一つ一つが新しい List 型に入るので、 "weather だけ取り出すべく、 json:"weather" というラベルを持つフィールドだけ用意します。
  3. (2) で "weather" に対応する値のマップは Weather 型のフィールド W に入れられることになります。 入れ物の中では "main""description" だけ受け取るように Weather 型を定義します。

組み合わせると

これを先ほどのアプリの中に /weather というエンドポイントをつけて実装してみます。

package main

import (
    "encoding/json"
    "html/template"
    "log"
    "net/http"
)

const API = "http://api.openweathermap.org/data/2.5/find?lat=35.6895&lon=139.6917&cnt=1" // (1)

type APIResult struct {
    L []List `json:"list"`
}

type List struct {
    W []Weather `json:"weather"`
}

type Weather struct {
    Main        string `json:"main"`
    Description string `json:"description"`
}

func (w Weather) String() string { // Weather型にメソッドを定義
    return w.Main + " (" + w.Description + ")"
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/goodbye", goodbyeHandler)
    http.HandleFunc("/weather", weatherHandler)
    http.ListenAndServe(":8080", nil)
}

const BaseHTML = `<!doctype html>
<p>{{ . }}`

var BaseTemplate = template.Must(template.New("base").Parse(BaseHTML))

func helloHandler(w http.ResponseWriter, r *http.Request) {
    BaseTemplate.Execute(w, "Hello, GTUG Girls")
}

func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
    BaseTemplate.Execute(w, "Good Bye, GTUG Girls")
}

func weatherHandler(w http.ResponseWriter, r *http.Request) {
    // GETリクエストでAPIを叩く
    resp, err := http.Get(API)
    if err != nil {
        log.Println(err)
        return
    }
    defer resp.Body.Close()

    // 結果のJSONをAPIResult型の変数に入れる
    var result APIResult
    decoder := json.NewDecoder(resp.Body)
    err = decoder.Decode(&result) // 参照渡し (1)
    if err != nil {
        log.Println(err)
        return
    }
    log.Println(result)

    // 結果を "天気名 (細かい説明)" という形で表示する
    BaseTemplate.Execute(w, result.L[0].W[0].String()) // メソッド呼び出し
}

ここで1つ新しいことが出てきたので見てみましょう。

  1. Goではスライスとマップ以外は基本的に「値渡し」になっています。
    • 変数を関数に渡して、返り値の形でなく更新してもらいたい場合は参照渡しにします。

演習問題3

ここまで完成させたプログラムをいよいよコンパイルしてみましょう。 次のコマンドを実行して、 go run で使った時と同様にアクセスできるか試してください。 また同様のPCの人同士(MBPの人同士、Win機同士)で作ったバイナリを交換して実行できるか試してください。

$ GOPATH=`pwd`; go build -o hello hello.go   # Linux/BSDの人
$ ./hello

演習問題4

先のプログラムでAPIに渡す変数で cnt の値を 10 に変更し、複数の結果を表示できるように改造してみましょう。

Note

ヒント: テンプレートでrangeとstructのフィールドにアクセスできる機能を使いましょう。 http://golang.org/pkg/text/template の "Variables" のセクションが参考になります。

goroutineとchannel

Goで一番おもしろい機能としてgoroutineとchannelがあります。 これは非同期のプログラムをスレッドやプロセスを深く意識することなく簡単に書けます。

ここからGoの面白いところなので、ぜひgoroutineとchannelを試してみてください。 これは A Tour of Go のサンプルがとてもわかりやすいです。

http://tour.golang.org/#63

これから先に進めたい人へ

Goは1.0のリリース後、急速にユースケースを増やしていて、これからがかなり期待される言語です。 興味を持った方はぜひその波に乗りましょう!

以下は気になった人向けの情報源です。

ドキュメント

日本語コミュニティ

英語コミュニティ

@Jxck

This comment has been minimized.

Jxck commented Sep 10, 2013

お疲れ様です。わかりやすくて良い資料ですね。

気になった点を。

map の初期化

Slice で

他にも組み込み関数の make を使った初期化方法がありますが、興味を持った場合に見てみてください。

としたのなら、 Map も make 無しで説明したほうが良いような気がします。

HTML を表示してみる

- var helloHTML = `<!doctype html><p style=">Hello, GTUG Girls` // (1)
+ var helloHTML = `<!doctype html><p>Hello, GTUG Girls` // (1)

あと、せっかく ` で囲むなら HTML は改行してもいいかもです。

HTTP リクエストをする

resp, err := http.Get(URL)   // (1)
if err != nil {
    return log.Println(err)
}
defer resp.Body.Close()      // (2)

return log.Println(err) は log.Println() used as value エラーですね。
返すか log のどっちかでいいと思います。log なら Fatal の方がいいかもです。

以上です。

当日はよろしくお願いいたします。

@ymotongpoo

This comment has been minimized.

Owner

ymotongpoo commented Sep 11, 2013

@Jxck 早速反映しました。ありがとうございます!
最後の log.Println(err) のところは完全に書き換える前のコードを変に流用したミスですね。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment