2016年2月26日金曜日

Go言語とGoogleAppEngineに触れる / Go言語

mainpicture

Go言語というものを仕事の関係上触ることになったので、そのキャッチアップした内容をまとめていきます。また、Google App Engine上での開発案件であったため、そこでハマったことなども合わせて書いていきます。

Go言語とGoogle App Engineに触れる

まずは、Go言語の雑感を簡単に。

  • 何の言語に似ているのかと言われれば、swiftに近いイメージ
  • でもかなりシンプル
  • 「go」「defer」「interface」「interface{}」「channel」が大切

幸いにして、Go言語にはとても手厚いチュートリアルが用意されています。

Go Tour

読みながら一通りやればある程度書けるようになります。素晴らしいですね!プログラミング言語の学習問わず、全ての学習をする上で「知ること」「聞くこと」「使うこと」という3ステップが大切です。Go言語の概要を知り、Go Tourを読み、動かしながら実際に書いていくようにしましょう!

ということで、実際にある程度やってみたので、その記録と感想を書きます。



Go Tour

だらだらと内容を併記しても内容が重複するので、気づいた箇所だけピックアップして記載していきます。

package

import (
    "fmt"
    "math/rand"
)

とかで記載します。ただ、そのコード内で利用していない場合はコンパイル時にエラーになります。ここが最初かなりしんどかった箇所です。

例えばデバッグ中にログを書き出したい時

import (
    "fmt"
)

func main() {
    v := 1
    v = v + 1
    fmt.Printf("The value is %v", v)    
}

これは大丈夫なのですが、

import (
    "fmt"
)

func main() {
    v := 1
    v = v + 1
}

これでエラーになります。毎回importまで戻って足したり消したりするのがめんどくさいです。 確かに不要なimportがなくなるのでコードは短くすっきりするし、依存関係も読みやすいのでありがたいところではあるのですが一長一短かと。

また、暗黙的な型宣言として

// これを
var v int = 1
// こう書ける
v := 1

といった記法があるのですが、良い点は型を意識しないでもある程度書けること、悪い点は何の型が入ってきているのかが分かりづらいことというのがあります。

例えば、

import (
    "google.golang.org/appengine"
)

func index(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    // 処理
}

これのcが何を表しているのかが分からないのです。何のパッケージをインストールしたらいいのやらという感じです。この段階だとコンパイルが通りますが、

import (
    "google.golang.org/appengine"
)

func index(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    sub(c)
}

func sub(c ????) {
    // 処理
}

この時点で結局importを書く必要があり、少し残念な気持ちになりました。

"golang.org/x/net/context"をimportすればcontextが解決され、???の箇所はcontext.Contextとすれば良いです。

function

頭文字を大文字にするとPublicの意味になります。

Go言語は高階関数を扱えるようになっていて、funcを引数に渡したり、closerを作ったりすることができます。javascriptやhaskellでも同様のことができますね。

実際に作成したCachedFinderを載せておきます。

package main

import "fmt"

func CachedFinder() func(key string) string {
    cache := make(map[string]string)
    return func(key string) string {
        str := cache[key]
        if str != "" {
            fmt.Println("got at cache")
            return str
        }
        str = "abc" + key // webapiやdb readなど
        if str == "" {
            return ""
        } else {
            cache[key] = str
            return str
        }
    }
}

func main() {
    fmt.Println("Hello, 世界")
    finder := CachedFinder()
    a := finder("hogehoge")
    fmt.Println(a)
    b := finder("piyopiyo")
    fmt.Println(b)
    c := finder("hogehoge")
    fmt.Println(c)
}

二度目のhogehogeはcacheから取得できるようになっています。

defer

deferをつけると、遅延呼び出しが可能になります。タイミングは呼び出し元の関数の終わり(returnする)まで遅延させるという機能です。

関数の引数はすぐに評価されるが、呼び出しがreturnの直後になる点に注意が必要です。deferが複数ある場合は、stackされるため実行は下(後に宣言した方)から呼ばれます。LIFO(last-in-first-out)です。

これに関しても良い点、悪い点があります。

deferの利用するポイントの一つとしては、Close()をNew()した直後に書くとかが多いですが、メソッドの終端で実行するという仕組みであるため、New()したものからさらにNew()をするという箇所では使えなかったのが残念でした。

Go App Engineのテストパッケージに"google.golang.org/appengine/aetest"というのがあり、InstanceからContextを生成する箇所があります。このContextだけ利用したいという箇所があるのですが、InstanceをCloseするとContextも使い物にならなくなるので、CreateInstance()みたいなのを定義しCloseは書けませんでした。

func TestHogehoge(t *testing.T) {
    opt := &aetest.Options{
        AppID: "test",
        StronglyConsistentDatastore: true,
    }
    inst, _ := aetest.NewInstance(opt)
    defer inst.Close()
    req, _ := inst.NewRequest("POST", "/", nil)
    c := appengine.NewContext(req)
    // 処理
}

これを関数として切り出して

func CreateInstance() context.Context {
    opt := &aetest.Options{
        AppID: "test",
        StronglyConsistentDatastore: true,
    }
    inst, _ := aetest.NewInstance(opt)
    defer inst.Close()
    req, _ := inst.NewRequest("POST", "/", nil)
    return appengine.NewContext(req)
}

とやると、使えないContextが出てきます。残念です。

ちなみに上記コードの意味はdatastoreの一貫性を強制的に保つようにするフラグを立ててContextを生成するものです。datastoreのテストをするとよくあるのが、writeの直後にreadすると値がreadできないという状況が発生するため、それを回避するために利用しています。

pointer

ポインタというか、Go言語は基本的には値渡しになります。なので、高コストのstructを生成したり、内容を変更したい時に使います。java界からきた私は最初少し違和感がありましたが、これはこれでまぁありかなという感じです。

例としては

package main

import "fmt"

func swap(s string) {
    s = "world" // これだとsは変更できない
}

func pswap(s *string) {
    *s = "world" // pointerを通して変更する
}

func main() {
    s := "hello"
    swap(s)
    fmt.Println(s)
    pswap(&s)
    fmt.Println(s)
}

slice

sliceがoverflowした時には新しい配列が割り当てられます。
以下二つの実行結果の違いで分かるかと思いますが、

package main

import "fmt"

func main() {
    fmt.Println("overflow")
    a := make([]int, 0, 5)
    a = append(a, 1, 2, 3, 4)
    printSlice("a", a)
    b := append(a, 1, 2, 3, 4)
    printSlice("b", b)
    a[0] = 100
    printSlice("a", a)
    printSlice("b", b)

    fmt.Println("not overflow")
    c := make([]int, 0, 100)
    c = append(c, 1, 2, 3, 4)
    printSlice("c", c)
    d := append(c, 1, 2, 3, 4)
    printSlice("d", d)
    c[0] = 100
    printSlice("c", c)
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

前者(a,b)はoverflowしたタイミングで別なsliceを割り当てるので、
aとbが最終的に示す0要素目の値が異なるようになります。

後者(c,d)の方はoverflowしない(makeでcapacityを100にしている)ので
最後のcとdのsliceの0要素目が示す値が同じ(100)になります。

なので、私が実装している時は基本的にappendの結果は自身で受け取るようにしています。

a = append(a, 1,2,3)
こんな感じですね。

methods

Goにはクラスは存在しません。struct型や任意の型にメソッドを定義できるので、メソッドレシーバー(funcの後に続く t *Typeなど)という呼ばれているようです。

interface

javaのinterfaceとかに似ていますね、単純なメソッドの集まりを定義していきます。

ただし、実装側は明示的にimplementsとか書かなくてよく、シグニチャレベルで一致しているかどうかの判断をしているようです。

type Abser interface {
    Abs() float64
}

func main() {
    var a Abser
    v := Vertex{3, 4}

    a = &v // a *Vertex implements Abser

    a = v
    fmt.Println(a.Abs())
}

type Vertex struct {
    X, Y float64
}

// これダメ。 *VertexがAbsを実装している定義となり、上の a = vでコケる。
// func (v *Vertex) Abs() float64 {
//  return math.Sqrt(v.X*v.X + v.Y*v.Y)
//}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

fmtパッケージにあるStringerというinterfaceがあり

type Stringer interface {
    String() string
}

という定義がされている。

そのため

type Person struct {
    Name string
    Age int 
}

func (p Person) String() string { // ここでStringerの実装
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
    a := Person("Taro", 10)
    fmt.Println(a)  
}

とかが可能になります。

reader

何か作る上ではfile,webstreamのreadを取り扱う必要があるのは避けられないことです。Go言語にはデータストリームを読むことを表現するio.Readerインターフェースが存在します。

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    r := strings.NewReader("Hello, Reader!")

    b := make([]byte, 8)
    for {
        n, err := r.Read(b)
        fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
        fmt.Printf("b[:n] = %q\n", b[:n])
        if err == io.EOF {
            break
        }
    }
}

tourそのままコピペしました。

出力結果が

n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""

こんな感じで出てきます。

Readerはbyte sliceに入れた数(n)とエラーを返しています。
fmt.Printfでbを見ると、きちんと1byte毎に分割されたHello, Reader!が出ていますね。

ASCII文字列の10進数で表現されているので対応表を載せておきました。
http://e-words.jp/p/r-ascii.html


さて以上が私がGo Tourをやってみた感想でした。

次回は、Google App Engineの開発でつまづいたところをまとめていきます。

最後に少しネタバレになりますが、Excersizeの解答例の一部を載せておきます。


Go Tour 回答例

Loops and Functions

package main

import (
    "fmt"
)

func Sqrt(x float64) float64 {
    z := 10000.0
    for i := 0; i < 10; i++ {
        z = z - (z * z - x) / (2 * z)
        fmt.Println(z)
    }
    return z
}

func main() {
    fmt.Println(Sqrt(2))
}

Slices

package main

import (
    "fmt"
    "golang.org/x/tour/pic"
)

func Pic(dx, dy int) [][]uint8 {
    fmt.Println(dx)
    fmt.Println(dy)
    ret := make([][]uint8, dy)
    for i := 0; i < dy; i++ {
        ret[i] = make([]uint8, dx)
        for j := 0; j < dx; j++ {
            ret[i][j] = uint8((i + j))
        }
    }
    return ret
}

func main() {
    pic.Show(Pic)
}

Maps

package main

import (
    "strings"
    "golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
    m := make(map[string]int)
    for _, v := range strings.Fields(s) {
        m[v] += 1
    }
    return m
}

func main() {
    wc.Test(WordCount)
}

Fibonacci closure

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
    v1 := 1
    v2 := 1
    return func() int {
        v1, v2 = v2, v1 + v2
        return v1
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

Stringers

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ipAddr IPAddr) String() string {
    return fmt.Sprintf("%v.%v.%v.%v", ipAddr[0], ipAddr[1], ipAddr[2], ipAddr[3]) 
}

func main() {
    addrs := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for n, a := range addrs {
        fmt.Printf("%v: %v\n", n, a)
    }
}

Errors

package main

import (
    "fmt"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
    return fmt.Sprint("cannot Sqrt negative number: %f\n",
                      float64(e))
}

func Sqrt(x float64) (float64, error) {
    if x < 0.0 {
        return 0, ErrNegativeSqrt(x)
    }
    z := 10000.0
    for i := 0; i < 10; i++ {
        z = z - (z * z - x) / (2 * z)
        fmt.Println(z)
    }
    return z, nil
}

func main() {
    fmt.Println(Sqrt(2))
    fmt.Println(Sqrt(-2))
}

Reader

package main

import (
    "strings"
    "golang.org/x/tour/reader"
)

type MyReader struct{}

// TODO: Add a Read([]byte) (int, error) method to MyReader.
func (mr MyReader) Read(b []byte) (int, error) {
    r := strings.NewReader("A")
    return r.Read(b)
}

func main() {
    reader.Validate(MyReader{})
}

rot13Reader

package main

import (
    "io"
    "os"
    "strings"
)

type rot13Reader struct {
    r io.Reader
}

func rot13(b byte) byte {
    if 'A' <= b && b <= 'Z' {
        k := b - 'A'
        return 'A' + (k-13+26)%26
    }
    if 'a' <= b && b <= 'z' {
        k := b - 'a'
        return 'a' + (k-13+26)%26
    }
    return b
}

func (rr rot13Reader) Read(b []byte) (int, error) {
    n, e := rr.r.Read(b)
    if e == io.EOF {
        return n, e
    }
    for i := 0; i < n; i++ {
        b[i] = rot13(b[i])
    }
    return n, e
}

func main() {
    s := strings.NewReader("Lbh penpxrq gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)
}

では良いインプットと良いプログラミングを。

0 件のコメント:

コメントを投稿