blog.takurinton.com

External

冷房壊れて3週間

2020/08/08

こんにちは

前回に引き続き、Treasureについての記事を書きます。まだエアコンが壊れているので家の中は地獄です。(๑˃̵ᴗ˂̵)

2日目の今日はGolangについての講義がありました。 自分自身Golang知らんって感じの人間なので、ヒイヒイゼエゼエしてました。また、Golang以外にもテスト駆動開発についての話や認証についての話についての講義もあり死ぬかと思いました。(生きてて良かった)

ちなみにさっき終わったばかりで何も理解してません。人に説明できるなって思ったところだけをこの記事には書いていきます。それ以外の部分はこれから復習していきます。(多分やった内容の10分の1くらいしか書けないけど)

では書いていきます。

TDD

TDDとは、テスト駆動開発のことで、最初に最低限のテストを実装してから開発をしていくスタイルのことを言います。 他にもSDD(締め切り駆動開発)やADD(あいみょん駆動開発)などがあります。

自分はGolangのコードを書いたことがなく、Pythonでしかテストコードを書いたことがないので苦戦しました。Pythonでのテストコードって言っても大したこと書かないので(今日も大したもの書いてないんだけど)言語の壁を感じました。メソッドとか文法真面目にやりまーす。

実際には

// unique.go

package etc

func Unique(elems []string) []string {

    result := make([]string, 0)
    encounter := map[string]bool{}

    for _, elem := range elems {
        if _, ok := encounter[elem]; !ok {
            result = append(result, elem)
            encounter[elem] = true
        }
    }
    return result
}
// unique_test.go

package etc

import (
    "fmt"
    "reflect"
    "testing"
)

func TestUnique(t *testing.T) {

    var cases = []struct {
        input  []string
        output []string
    }{
        {
            []string{"go", "js", "go"},
            []string{"go", "js"},
        },
        {
            []string{"takurinton", "takurinton", "takurinton"},
            []string{"takurinton"},
        },
        {
            []string{"go"},
            []string{"go"},
        },
    }

    for _, c := range cases {
        t.Run(fmt.Sprintf("%v", c.input), func(t *testing.T) {
            if want, got := c.output, Unique(c.input); !reflect.DeepEqual(want, got) {
                t.Fatalf("Want %v but got %v", want, got)
            }
        })
    }
}

こんな感じで実装してます。 なんかようわからんですね。 やってることとすれば、ダブってるやつを排除するというプログラムの出力が正しいかをテストしました。コード自体は被ってたら追加しない、なかったら追加するって感じです。Pythonでいうset関数みたいなのはGolangにはないらしい。ふ、Pythonの勝ちじゃ。

テストを実行する側は、TestUniqueという関数で実行をしています。inputとoutput(今回はスライス)で定義をしています。outputのスライスの中にさらにスライスを作ることで簡単に処理をすることを心がけました。

テストについてはいろいろなものがありますが、TDDは割と一般的なのかなとか思ってます。

REST API について

レストっぽさというものを学びました(理解はしてない) 考えることが多すぎる、あとはここら辺から徐々についていけなくなります。

まずHTTPについて

  • requestの種類

    • GET: 読み込む
    • POST: 新規作成
    • PUT/PATCH: 丸ごと変更 / 部分変更
    • DELETE: 消す
  • ステータスコード

    • 2xx: よかよか
    • 3xx: 邪魔じゃ!
    • 4xx: ユーザ側(クライアント側)の問題
    • 5xx: サーバ側の問題
  • エンドポイント

    • ここにrequestを投げてデータのやり取りをする
    • requestの種類と中身、認証の情報によってステータスコードと必要であれば情報を返す

ここら辺の知識を使って設計をしていきます。 基本的にはヘルスチェックのことについて書いています。

サーバのエラーとDBのエラーは別!

サーバのエラーとDBのエラーは別という話がありました。 試しに、docker-composeで立ち上げたサーバからDBだけ殺し、pingというテスト用のエンドポイントにrequestを投げると200が返ってきます。こりゃいけない。 ということで、サーバサイドにはDBが死んでるかどうかを確かめるための処理を追加しないといけません。 例として下のようなエンドポイントがあるとします。

r.Methods(http.MethodGet).Path("/ping").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
})

上記のコードを使用すれば/pingというエンドポイントにアクセスして、オッケーなら200を返してくれます。しかし、これだけだとDBが死んでても200です。 そこで以下のように変更します。

r.Methods(http.MethodGet).Path("/ping").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if err := s.db.Ping(); err != nil {
            log.Printf("failed to ping by error '%#v'", err)
            w.WriteHeader(500)
            fmt.Fprintf(w, "HTTP 500 internal")
        } else {
            w.WriteHeader(200)
            fmt.Fprintf(w, "HTTP 200 ok")
        }
})

このようにすることでDBのエラーも検知することができます。 ちなみにこれの s.db.Ping()についてはこちらを参照してもらえると出てきます。sについてはだいぶ上の方で定めてるserverの変数のことです。

RESTでは共通意識を持って!

レストフルの設計は設計する人間に依存しますが、とある人が提唱してるレストフルな設計というものがあります。 レベルで分けられていて、大体このインターンではレベル2くらいまで満たせてればいいっぽいですが、僕は高みを目指したい(切実) ちょっとレベルについて紹介します。

  • level0: 単純なHTTP
  • level1: リソース
  • level2: GET, POST, PUT(PATCH), DELETE
    • get room/{id}/delete よりも delete room/{id}
    • どっちでもいけるけど、後者の方がRESTっぽい!

こんな感じです。レベル3になると、bodyの中に自分自身の情報が入っていて、それを使用してさらにrequestを送ることができたりと発展していきます。まずは普通のやつからやっていきましょう。

また、小技として、エンドポイントの名前に単数形を使うか複数形を使うかという話が出ました。(/room か /rooms かみたいな) その時は単数形を使うといいみたいです。理由はmanは複数形になるとmenになるから。深い。英語わかんない。

サーバサイドのエラーをかくせ!

今度はサーバサイドでのエラーはなるべくクライアント側から見えないようにしよう!という話です。

サーバサイドのエラーというのは無意識に大切な情報をクライアントに返してしまったりします。 そのため、エラーが出た時には置換することが大事です。ステータスコードは返すけど、大切な情報は返さないことが基本です。

そのような時は以下のように記述します。これはステータスコードが500以上の時にInternal Server Errorを返すためのプログラムです。

func RespondErrorJson(w http.ResponseWriter, code int, err error) {
    log.Printf("code=%d, err=%s", code, err)
    if code >= 500 {
        RespondJSON(w, code, HTTPError{Message: http.StatusText(code)})
    } else if e, ok := err.(*HTTPError); ok {
        RespondJSON(w, code, e)
    } else if err != nil {
        he := HTTPError{
            Message: err.Error(),
        }
        RespondJSON(w, code, he)
    }
}

うんなるほど、何もわからん。 これはcodeというint型の引数でステータスコードを受け取り、それが500以上だったらHTTPErrorを返すと言ったものです。RespondJSONはだいぶ上の方で定義してあります。(さっきから省略しすぎだろって意見はやめてください。これでも頑張ってるんです( ;∀;))

このあとテストコードを書いて正しい処理が返ってきて感動しました。

データベースについて

データベースについてはほぼわかってません。 たくさん話し合いをしましたが、いまいち掴めませんでした。 とりあえず、僕はDjangoにお世話になり過ぎていました。migrationが通らないと思ったらフォルダ間違えてたり、焦ってわけわからん設計して時間を溶かしてしまったので、後日じっくりと一人の時間を作って取り組みたいと思います。

まとめ

最後の方雑になってしまいましたが、ただ理解してないだけでこれが僕の限界です。情けない。 あとは、講義内容はもっと深く、そして様々な質問や議論が飛び交っていました。 今日はあっという間に過ぎすぎてわけわからんかったので、今週はTreasureフルコミットで30回くらいビデオを見直し、復習とできれば予習くらいまでできるように頑張りたいと思います!

圧倒的成長するぞ〜!おー!