20201207のGoに関する記事は15件です。

Goでゲームのロビー機能のようなモノを作る

はじめに

こちらは Go2 Advent Calendar 2020 9日目の記事です。

ゲームのマッチングを行うロビー機能のようなモノをGoで作ってみたので、紹介と簡単な解説をしたいと思います。
ログインしたらロビーでマッチングするまで待機し、相手が見つかったらゲーム画面に接続するというモノです。今回作った内容に、認証機能は含まれていません。

作ったもの

connect4という1対1のゲームをベースにしています。
プレイヤー名を入力してログインすると、ロビー(マッチング画面)に移動します。
プレイヤーが2名揃うとプレイ画面が表示されて、ゲームが始まります。

connect4play.gif

仕組み

次の3つのページで構成しています。
- ログインページ:プレイヤー名を入力してログインするためのページ
- ロビーページ:対戦相手のマッチング待つ待機用のページ
- ゲーム画面

ログインではクライアント-サーバ間でセッションを作ります。
サーバにプレイヤー名を登録して、クライアントにセッションIDを発行しています。
同時にWebSocketのペアを用意してプレイヤーに割り当てます。

ロビーでは、他のプレイヤーが同じWebSocketのペアに割り当てられるのを待機します。
割り当てが終わるとゲーム画面に移動します。

ゲーム画面では割り当てられたWebSocketのペアで通信してゲームを進めます。

gameroom.png

func main() {
    flag.Parse()
    http.HandleFunc("/", serveFront)  // ログインページ
    http.HandleFunc("/lobby", serveLobby) // ロビーページ
    http.HandleFunc("/play", servePlay) // ゲーム画面
    http.HandleFunc("/login", serveLoginHandler) // Session開始とWebSocketペアの発行
    http.HandleFunc("/ws", serveWebsocket) // WebSocket処理
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

セッション管理

セッションの構造体定義は以下の通りです。
Cookieを使ってsessionIDでやり取りします。Sessionの更新はManagerを介して行います。

type Session struct {
    cookieName string
    ID         string
    manager    *Manager
    Values     map[string]interface{}
    writer     http.ResponseWriter
}

セッションを開始する

セッション管理はManagerで行っていて、manager.Start()でセッションを開始します。この中ではcookieNameと*http.Requestのcookie情報を基にセッションを取得しています。このとき、既にセッションが確立していれば、既存のセッションを返します。
セッション情報がない場合は、ログインに使ったプレイヤー名をセッションに登録して保存します。このときhttp.responseWriterにcookie情報をセットして返します。

これでクライアントにセッションIDを発行できました。

// セッションを開始
manager := sessions.NewManager()
session, err := manager.Start(w, r, cookieName)
if err != nil {
    http.Error(w, "session start faild", http.StatusMethodNotAllowed)
    return
}
session.Set("account", r.FormValue("account"))
if err := session.Save(); err != nil {
    http.Error(w, "session save faild", http.StatusMethodNotAllowed)
    return
}

WebSocket通信

WebSocket通信はgorilla/websocketを使っています。
単一のWebSocket通信はexamplesを参考にしているので、そちらを見てください。
hubに接続したクライアント同士がWebSocketのペアとなり、メッセージを受け取れるようになっています。

単一のWebSocketペアはhubで管理されていますが、複数のPlayルームを用意したいので複数のWebSocketペアをManagerを作って管理します。Managerの構造体は次のようになっています。

type Manager struct {
    database map[string]*Hub
    pool     []*Hub
    count    map[*Hub]int
    users    map[*Hub][]string
}

サーバは複数のhubpoolに持っていて、割り当てを要求されたらpoolの先頭から順に割り当てるという単純な作りです。hubの定員に達したらpoolから除外していきます。今回は1対1のゲームなので、hubの定員は2名です。

func (m *Manager) Get(key string) (*Hub, error) {
    if hub, ok := m.database[key]; ok {
        return hub, nil
    }
    if len(m.pool) <= 0 {
        return nil, fmt.Errorf("no hub")
    }

    hub := m.pool[0] // 先頭のHubを割り当てる
    m.database[key] = hub
    m.count[hub]++
    if m.count[hub] >= PoolMax {
        m.pool = m.pool[1:] // 先頭のHubを除外する
    }
    m.users[hub] = append(m.users[hub], key)
    return hub, nil
}

サーバはhubが定員に達したら、マッチング完了を待機中のクライアントに対してWebSocketで通知し、クライアントは通知を受け取るとゲーム画面に移動します。

今後について

ログイン時に入力したユーザ名が活用できていないのと、使い終わったhubpoolに戻す処理、終了処理が未対応なので、これらは近いうち作ろうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

安く簡単にGo製webサーバーをデプロイする! Render.comの紹介

Go言語で個人開発したサーバーをGCPやAWSにデプロイするときの悩みは、各プラットフォームの必要知識が意外にも多いことと、地味に出費がかさむことです。
今回は、余計なプラットフォームの設定で悩むことなくアプリケーションに集中できるうえ、低価格で実現できるサービスを紹介します。

私は2020年10月からクラスター社でエンジニアインターンをしている @anoriqq です。
日々、サーバーエンジニア修行を積んでいます。

さて、この記事は クラスター Advent Calendar 2020 9日目の記事です。
昨日はnomunomu_raさんの『クラスター社員が語る、バーチャルカメラの魅力。』でした。
clusterでイベントを開催する方は必見です!

はじめに

クラスター社の提供するサービスclusterのサーバーにGoを利用しています。
しかし、私自身のGo言語でのサービス開発は初めてでしたので、実際にサービスに使われているライブラリやフレームワークを使ったオモチャが欲しくなります。
そこで、Go製のwebサーバーがデプロイできるRenderを使ってみたところ、GCPやAWS、Herokuよりも使い勝手が良いと感じたのでご紹介します。

Renderってどんなサービス?

image.png
Webアプリや静的サイトのホスティングから、Cron Jobs、自動バックアップ機能付きDBなど、個人のWeb開発で必要なものは一通り揃っています。
SSDディスクも提供されていますので、PostgreSQL意外ももちろん使えます。
無料でonrender.comのサブドメインも割り当ててくれますし、カスタムドメインも追加できます。

image.png
ドキュメントもそれなりに詳しく、サンプルプロジェクト付きで解説があります。
個別に対応していない言語もありますが、Dockerが動くのでなんでもできますね。

RenderでGo製サーバーを動かそう

さて、本題のGo言語プロジェクトですが、デプロイに必要な作業はたったの3ステップです。

  1. サービスタイプを選択

Webサーバーの場合はWeb serversを選ぶと良いです。
Background WorkerはURLを持ちません。
Screen Shot 2020-12-07 at 22.01.57.png

  1. Gitリポジトリを選択する

GitHubかGitLabからプロジェクトのリポジトリを選択できます。
Screen Shot 2020-12-07 at 22.03.42.png

  1. サービスの設定

EnvironmentにGoを選択し、Build CommandとStart Commandを指定します。
Regionは現在 Oregon, USA と Frankfurt, Germany が選択できるようですが、日本からだとUSAのほうが速いです。
デフォルトでAuto Deployが有効になっているため、Branchに指定したBranchにpushされると自動的にデプロイされます。
もちろん手動デプロイも可能です。
その他、環境変数やシークレットファイル、追加の永続ディスクなどもこの画面で設定できます。
Screen Shot 2020-12-07 at 22.06.21.png

良いところ1: デプロイが速い

Dec 7 10:12:20 PM  ==> Cloning
Dec 7 10:12:26 PM  ==> Running build command
Dec 7 10:12:53 PM  ==> Build successful ?
Dec 7 10:13:10 PM  ==> Starting service with './app'
Dec 7 10:13:35 PM  [GIN] 2020/12/07 - 13:13:35 | 200 |    2.617112ms |   111.111.111.111 | GET      "/"

あるプロジェクトのlogの抜粋です。
buildはプロジェクトの規模で変わると思いますが、build完了後30秒程度でアクセス可能になります。すごい!!
chackoutにはキャッシュも効かせてくれます。

良いところ2: 安心できる価格設定

Screen Shot 2020-12-07 at 22.23.34.png

今回利用したサービスタイプWeb serversでは最も安いStarterプランで最高 $7/月 です。
しかも利用した分だけ課金されるので無駄に出費する心配もありません。
私の個人開発は趣味や勉強目的なので、サービスを作ったり消したりしていますが毎月300円程度しか請求されていません。

おわりに

様々なクラウドコンピューティングサービスを利用した中で、最も入門しやすく、個人開発に十分な機能を備えているので、知らない人には広めていきたいですね。
来年もRenderで個人開発とスキルアップを加速していくぞ!

明日はfaidraさんの『なにか』です。
楽しみです!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語をインストールしない Go 言語開発環境

Go言語(go, golang)をインストールすることなく、Go言語の開発環境を用意する方法です。
簡単に言うと、使い捨ての Docker コンテナ上で go コマンドを実行します。
golang インストールは不要ですが、基本的に GOPATH=~/go 以下にプロジェクトのソースディレクトリがある事を前提とします。
※ Docker を利用するので Docker のインストールは必要です。

動作確認環境: macOS Catalina 10.15.8

Atom + go-plus で開発していますが、保存時のテスト実行等も問題なく動作しているようです。

サンプルは go mod init ${PWD:$(echo “$GPATH/src”|wc -c)} を実行する例です。

プライベートリポジトリなしの場合

docker run --rm -v "$PWD":"${PWD:$#HOME}" -w "${PWD:$#HOME}" golang:1.14.12 go mod init ${PWD:$(echo “$GPATH/src”|wc -c)}

プライベートリポジトリを含む場合

プライベートリポジトリが https://privatte-repo の場合に対応しています。

docker run —rm -v "$HOME/.ssh":"/root/.ssh":ro -v “$PWD”:”${PWD:$#HOME}” -w “${PWD:$#HOME}” --env GOPRIVATE="private-repo" golang:1.14.12 /bin/sh -c "git config --global url.ssh://git@private-repo.insteadOf https://private-repo && go mod init ${PWD:$(echo “$GPATH/src”|wc -c)}"

git config で特定のリポジトリに対して go get を ssh 接続に切り替えるため、コンテナで go コマンドを含む複数コマンド実行が必要なので、/bin/sh-c オプションを利用しています。

エイリアスを登録して利用

1行で書けますが非常に長いので、実際に利用する時は関数を用意してエイリアスを登録して利用しています。

~/.zshrc
alias go='golang'
alias goinit='golang mod init ${PWD:$(echo “$GPATH/src”|wc -c)}"'

golang() {
  USE_PRIVATE_REPO="git config --global url.ssh://git@private-repo.insteadOf https://private-repo"
  GOPRIVATE="private-repo"
  WORKDIR=$(echo "${PWD}" | awk '{print substr($0, index($0, "/go/"))}')
  docker run --rm                   \
  -v "${HOME}/.ssh":"/root/.ssh":ro \
  -v "${PWD}":"${WORKDIR}"          \
  -w "${WORKDIR}"                   \
  -e GOPRIVATE                      \
  golang:1.14.12 /bin/sh -c "${USE_PRIVATE_REPO} && go $*"
}
source ~/.zshrc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語をインストールしない Go言語開発環境構築

Go言語(go, golang)をインストールすることなく、Go言語の開発環境を構築する方法です。
簡単に言うと、使い捨ての Docker コンテナ上で go コマンドを実行します。
golang インストールは不要ですが、基本的に GOPATH=~/go 以下にプロジェクトのソースディレクトリがある事を前提とします。
※ Docker を利用するので Docker のインストールは必要です。

動作確認環境: macOS Catalina 10.15.8

Atom + go-plus で開発していますが、保存時のテスト実行等も問題なく動作しているようです。

サンプルは go mod init ${PWD:$(echo “$GPATH/src”|wc -c)} を実行する例です。

プライベートリポジトリなしの場合

docker run --rm -v "$PWD":"${PWD:$#HOME}" -w "${PWD:$#HOME}" golang:1.14.12 go mod init ${PWD:$(echo “$GPATH/src”|wc -c)}

プライベートリポジトリを含む場合

プライベートリポジトリが https://privatte-repo の場合に対応しています。

docker run —rm -v "$HOME/.ssh":"/root/.ssh":ro -v “$PWD”:”${PWD:$#HOME}” -w “${PWD:$#HOME}” --env GOPRIVATE="private-repo" golang:1.14.12 /bin/sh -c "git config --global url.ssh://git@private-repo.insteadOf https://private-repo && go mod init ${PWD:$(echo “$GPATH/src”|wc -c)}"

git config で特定のリポジトリに対して go get を ssh 接続に切り替えるため、コンテナで go コマンドを含む複数コマンド実行が必要なので、/bin/sh-c オプションを利用しています。

エイリアスを登録して利用

1行で書けますが非常に長いので、実際に利用する時は関数を用意してエイリアスを登録して利用しています。

~/.zshrc
alias go='golang'
alias goinit='golang mod init ${PWD:$(echo “$GPATH/src”|wc -c)}"'

golang() {
  USE_PRIVATE_REPO="git config --global url.ssh://git@private-repo.insteadOf https://private-repo"
  GOPRIVATE="private-repo"
  WORKDIR=$(echo "${PWD}" | awk '{print substr($0, index($0, "/go/"))}')
  docker run --rm                   \
  -v "${HOME}/.ssh":"/root/.ssh":ro \
  -v "${PWD}":"${WORKDIR}"          \
  -w "${WORKDIR}"                   \
  -e GOPRIVATE                      \
  golang:1.14.12 /bin/sh -c "${USE_PRIVATE_REPO} && go $*"
}
source ~/.zshrc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeetCodeに毎日挑戦してみた 70. Climbing Stairs(Python、Go)

はじめに

無料英単語サイトE-tanを運営中の@ishishowです。

プログラマとしての能力を上げるために毎日leetcodeに取り組み、自分なりの解き方を挙げていきたいと思います。

Leetcodeとは

leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。

golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)

18問目(問題70)

70. Climbing Stairs

問題内容

You are climbing a staircase. It takes n steps to reach the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

(日本語訳)

あなたは階段を上っています。nトップに到達するためのステップが必要です。

1 登ったり、2足を踏み入れたりするたびに。いくつの明確な方法でトップに登ることができますか?

Example 1:

Input: n = 2
Output: 2
Explanation: There are two ways to climb to the top.
1. 1 step + 1 step
2. 2 steps

Example 2:

Input: n = 3
Output: 3
Explanation: There are three ways to climb to the top.
1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step

考え方

  1. こちら冷静に考えてみるとn番目のフィボナッチ数を求めるのと同じ意味で出題されているとわかります。

  2. 今回はループ処理で解いてみました。再帰的に説くことも可能です。

  • 解答コード
class Solution:
    def climbStairs(self, n):
        a, b = 1, 1
        for i in range(n):
            tmp = b
            b = a + b
            a = tmp

        return a
  • Goでも書いてみます!
func climbStairs(n int) int {
    a := 1
    b := 1
    tmp := 0
    for i := 0; i < n; i++ {
        tmp = b
        b = a + b
        a = tmp
    }
    return a
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go言語で学ぶ】抽象化

皆さんこんにちは!
シュークリーム大好きエンジニアのくろちゃんです。

こちらの記事は、CA21 Advent Calendar 2020の7日目の記事です!
最近QiitaやTwitterでのアウトプットが全くできていなかったので、アドベントカレンダーを良いきっかけにできたら嬉しいなと思っています:muscle:

背景

実は、3週間ほど前から23卒サーバサイドエンジニアの子を鍛え上げるというプロジェクトでメンターをやらせていただいています。

当面の目標をGo言語を用いたAPIサーバ開発ができるようになることを目標に置き、Go言語の基本構文から勉強してもらっています。

その中で、抽象化について学んでもらおうと思った際に上手い教え方ができず、結局何が便利だから抽象化するんだっけ?というところまで伝えられませんでした。

この体験を元に、自分自身抽象化についてもう一度学び直しました。
この記事を読んだ23卒の子が抽象化について少しでも理解を深めてくれたら良いなと思って記事にまとめます。

そもそも抽象化って?

プログラミング以外の場面でも、「抽象化して考えてみよう!」などと言ったりしますが、そもそも抽象化とは具体的に何をしたら抽象化できた事になるのでしょうか?

まずは抽象化の意味について調べてみました。

▼抽象化の意味(Wikipediaから引用)

思考における手法のひとつで、対象から注目すべき要素を重点的に抜き出して他は捨て去る方法である

何だかわかりそうで分からない・・・・。
個人的な解釈でいうと、要は「グルーピングする事」なのではないかと考えています。

例えば、自動車を抽象化する時のことを考えてみましょう。
まずは、具体的な自動車を2台頭に浮かべます。スポーツカーと軽自動車を思い浮かべてみてください。

スポーツカーと軽自動車.png

この2台比較したときに共通の要素を抜き出してみてください。

  • アクセルを踏むと前進する
  • ブレーキを踏むと止まる
  • ライトが付いている
  • 乗るには免許証が必要

などでしょうか?今回はスポーツカーと軽自動車を比べましたが、ここにトラックや電気自動車なども加えることで、共通する部分が抜き出しやすくなるのではないでしょうか。

上記で挙げたような要素を満たすモノのことを私たちは一般的に車や自動車と呼んでいますね?
単体で見れば、スポーツカーと軽自動車では出せるスピードが違ったり、燃費も違ったりします。

ですが、共通点として挙げた「アクセルを踏むと前進する」といった特徴はスポーツカーや軽自動車特有の特徴ではありません。
自動車というグループに所属するモノ全てが持ち合わせている特徴です。

僕はこのように、具体的な1つ1つのモノ同士を比べて共通点を探し、グルーピングする事が抽象化するという事だと考えています。

抽象化.png

プログラミングにおける抽象化

前章で抽象化そのものの考え方について見てきました。
プログラミングにおける抽象化も、先ほど説明した考え方とほぼほぼ一緒で、基本的には共通項を切り出してグルーピングするというモノになります。

ですが、プログラミングにおける抽象化はどちらかというと、実装の詳細を隠蔽化して利用できるようにするという文脈で使われる事が多いように感じており、そこを説明しなかったからこそ、今回抽象化の必要性を感じてもらえなかったのかなーと考えています。

ここからは、Go言語のサンプルコードをお見せしながら、

  • 共通項を切り出してグルーピングする抽象化
  • 実装の詳細を隠蔽する抽象化

の2つについて詳しく説明します!

共通項を切り出してグルーピングする抽象化

こちらの抽象化は、僕たちが普段日常生活などでも行っている抽象化に近い考え方であるため、比較的簡単にイメージする事が可能です。
しかし、抽象化をするメリットがよく分からないという落とし穴にハマりがちなのかなとも感じています。

僕がトレーニーから「抽象化については何となくわかったけれど、何が便利なのか分かりません!」と言われた例を示します。よくある、動物を題材に取り上げたサンプルコードとなっています。

animal/interface.go
package animal

type Animal interface {
    Bark()
    Eat()
    Die()
}

まずは、動物というグルーピングをした時に、

  • 鳴く
  • 食べる
  • 死ぬ

という3つの動作は共通化して切り出せそうなので、interfaceとして切り出します。
次に、animalインターフェースを満たすDogCatの詳細実装をします。

dog/dog.go
package dog

import (
    "fmt"
    "interface/animal"
)

type Dog struct {
    Name         string
    Age          int
    FavoriteFood string
}

func New() animal.Animal {
    return &Dog{
        Name:         "taro",
        Age:          10,
        FavoriteFood: "dog food",
    }
}

func (d *Dog) Bark() {
    fmt.Println("わん!")
}

func (d *Dog) Eat() {
    fmt.Printf("%sは大好物の%sを食べた\n", d.Name, d.FavoriteFood)
}

func (d *Dog) Die() {
    fmt.Printf("%sは%d歳で生涯を終えた\n", d.Name, d.Age)
}
cat/cat.go
package cat

import (
    "fmt"
    "interface/animal"
)

type Cat struct {
    Name         string
    Age          int
    FavoriteFood string
}

func New() animal.Animal {
    return &Cat{
        Name:         "tama",
        Age:          8,
        FavoriteFood: "マグロ",
    }
}

func (c *Cat) Bark() {
    fmt.Println("ニャァ!")
}

func (c *Cat) Eat() {
    fmt.Printf("%sは大好物の%sを食べた。\n", c.Name, c.FavoriteFood)
}

func (c *Cat) Die() {
    fmt.Printf("%sは%d歳で生涯を終えた\n", c.Name, c.Age)
}

Go言語の場合、Javaなどに見られるimplementsは存在せず、独自の型(今回で言うとstruct)をインターフェースにキャストするタイミングで自動的にインターフェースを満たしているかをチェックしてくれる。(←今回の例だとNew()にあたります)

※インターフェースを満たしている = interfaceで定義したメソッドを全て持っている状態。
インターフェースを満たしていない場合:下記のようなコンパイルエラーになる!

error.log
dog/dog.go:15:9: cannot use &Dog literal (type *Dog) as type animal.Animal in return argument:
*Dog does not implement animal.Animal (missing Bark method)

main関数では、単純にdogとcatのNew()を使ってインスタンスを生成し、生成したインスタンスの持っているメソッド(Bark(), Eat(), Die())を呼び出しています。

main.go
package main

import (
    "interface/cat"
    "interface/dog"
)

func main() {
    animal1 := dog.New()
    animal2 := cat.New()

    animal1.Bark()
    animal2.Bark()

    animal1.Eat()
    animal2.Eat()

    animal1.Die()
    animal2.Die()
}

いかがでしょうか?
共通項を切り出してグルーピングする抽象化についてイメージを持っていただけましたでしょうか?

今回の場合で言うと、犬と猫それぞれの共通点を考え、鳴く・食べる・死ぬと言う3つの振る舞いを取り出しました。
この時、鳴く・食べる・死ぬと言う振る舞いは犬と猫に限った話ではなく、動物というグループで見たときも同じ事が言えます。

そこで、鳴く・食べる・死ぬという振る舞いをまとめてanimalというインターフェースにまとめました。
現実世界での抽象化と同じような思考プロセスを辿るため、比較的すんなり理解できたのではないでしょうか?

しかし、この例では抽象化という考え方は学べても、プログラミングする上で何が嬉しいのか正直よく分かりません。(よね?)

実装の詳細を隠蔽する抽象化

個人的にプログラミングする中で得られる抽象化のメリットと感じているのは、実装の詳細を隠蔽して、関数を使わせることができることだと思っています。

実装の詳細とは、関数内部で行っている具体的な処理のことを指します。
例えば、ユーザを作成するという機能について考えたときの実装の詳細は下記のような一連の処理を指します。

  1. ユーザから受け取った値をバリデーションする
  2. UUIDを生成する
  3. ユーザモデルにマッピングする
  4. MySQLのUserテーブルに対してINSERTを実行する

この時、この関数を使う側からすると、「中身の処理がどうなっていようと、そんなことはどうでもいいからユーザを作成してください!」と思うのではないでしょうか?

実装の詳細を隠蔽することで得られる実装上のメリット

開発者にとっても、実装詳細の隠蔽化をすることで得られるメリットがあります。
それは、コードの柔軟性を上げられるというメリットです!

まずは、設計者になったつもりで次のようなことを考えてみましょう。
ユーザに関するいくつかの仕様が固まったとします。今回は仮に、

  • ユーザ作成
  • ユーザ情報取得
  • ユーザ情報更新
  • ユーザと講座情報紐付け

という4つの機能を実装することになりました。
この時、設計者であるあなたが仕様通りの機能を全て実装するのは非常にナンセンスです。

できればチームメンバーに中身の実装はお願いしたいところですね!
そんな時役立つのが抽象化です!

設計者であるあなたは、上記4つの機能の入力と出力だけ定めたインターフェースを作成するだけで、あとの中身の実装は他の人に任せることができます。

任された人も、インターフェースを満たすようにさえすれば良いので、機能ごとの詳細な実装に集中することができます。

また、インターフェースを満たすものであれば同じインターフェースから複数個インスタンスが生成できるため、MySQLへの操作に使うインスタンスRedisへの操作を行うインスタンスなどのように、中で使用している技術に応じてインスタンスを分けていくことが可能です。

そうしておけば、使う側も用途に合わせて適切なインスタンスを生成し、利用することで「あくまでユーザを作成するなどのような粒度」で扱うことができます。

上記のような抽象化のメリットを少しでも感じていただけるように、サンプルコードを作成しました。
GitHubにてコードを公開していますので、そちらも合わせてご覧ください。

まずはUserInterfaceという名前のインターフェースを定義します。

type UserInterface interface {
    PrintMyData()
    UpdateBaseData(entity.UserData)
}

これを見ただけで、ユーザは自分のデータを出力する機能基本データを更新するための機能が存在することが分かりますね。

このインターフェースの詳細実装を見ていきましょう。
今回実装されているのは、adultchildの2つです。

adult.go
package adult

import (
    "encoding/json"
    "fmt"
    "golearninterface/interface/user"
    "golearninterface/model/user/entity"
    "io/ioutil"
    "log"
)

const filename = "user.json"

type Adult struct {
    entity.UserData
    Phone   string `json:"phone"`
    Married bool   `json:"married"`
}

func New(userData entity.UserData, phone string, married bool) user.UserInterface {
    return &Adult{
        UserData: userData,
        Phone:    phone,
        Married:  married,
    }
}

func (a *Adult) PrintMyData() {
    if a.Age <= 18 {
        fmt.Println("18歳以下の方はご利用になれません.")
        return
    }
    file, err := json.MarshalIndent(a, "", "  ")
    if err != nil {
        log.Fatalln(err)
    }

    if err := ioutil.WriteFile(filename, file, 0644); err != nil {
        log.Fatalln(err)
    }
    fmt.Printf("%s様のデータを %s に保存しました.\n", a.Name, filename)
}

func (a *Adult) UpdateBaseData(data entity.UserData) {
    if a.Age <= 18 {
        fmt.Println("18歳以下の方はご利用になれません.")
        return
    }
    a.UserData = data
}
child.go
package child

import (
    "fmt"
    "golearninterface/interface/user"
    "golearninterface/model/user/entity"
)

type GenderType = int

const (
    Man     GenderType = 1
    Woman   GenderType = 2
    Unknown GenderType = 0
)

type Child struct {
    entity.UserData
    Gender GenderType `json:"gender"`
}

func New(userData entity.UserData, gender GenderType) user.UserInterface {
    return &Child{
        UserData: userData,
        Gender:   gender,
    }
}

func (c *Child) PrintMyData() {
    if c.Age > 18 {
        fmt.Println("19歳以上の方はご利用になれません.")
        return
    }

    switch c.Gender {
    case Man:
        fmt.Printf("ようこそ!%sさん(♂).あなたの誕生日は%sですね!\n", c.Name, c.Birthday)
    case Woman:
        fmt.Printf("ようこそ!%sさん(♀).あなたの誕生日は%sですね!\n", c.Name, c.Birthday)
    case Unknown:
        fmt.Printf("ようこそ!%sさん.あなたの誕生日は%sですね!\n", c.Name, c.Birthday)
    }
}

func (c *Child) UpdateBaseData(data entity.UserData) {
    if c.Age > 18 {
        fmt.Println("19歳以上の方はご利用になれません.")
        return
    }
    c.UserData = data
}

Adult(大人)とChild(子供)で全く異なる実装がされていることがわかりますか?

まずは、PrintMyData()について。
もちろん大人と子供で年齢の条件が異なるため、別々のバリデーションがかかっています。

また、Adult(大人)は自分自身のデータをJSON形式に変換してuser.jsonという名前のファイルに出力しているのに対して、Child(子供)は性別に合わせて文字列を生成し、標準出力にプリントしています。

異なるのは関数の内部処理だけではありません。
大人と子供で構造体の中身が違うことがわかると思います。

adult.go
type Adult struct {
    entity.UserData
    Phone   string `json:"phone"`
    Married bool   `json:"married"`
}
child.go
type Child struct {
    entity.UserData
    Gender GenderType `json:"gender"`
}

インターフェースを基にしてそれぞれ異なった構造体をもち、それぞれの要件に合わせた実装がされていると見ることができます。

これらの生成されたインスタンスを使う側は、特に実装の中身を意識することなく、生成したインスタンスで公開されている関数を利用することができます。

main.go
package main

import (
    "fmt"
    "golearninterface/model/user/adult"
    "golearninterface/model/user/child"
    "golearninterface/model/user/entity"
)

func main() {
    fmt.Println("-------- Child --------")
    childUser := child.New(
        entity.UserData{
            Name:     "kuro",
            Age:      18,
            Birthday: "2001/12/07",
        },
        child.Man,
    )
    childUser.PrintMyData()
    childUser.UpdateBaseData(
        entity.UserData{
            Name:     "黒澤",
            Age:      17,
            Birthday: "1998/03/07",
        },
    )
    childUser.PrintMyData()

    fmt.Println("-------- Adult --------")
    adultUser := adult.New(
        entity.UserData{
            Name:     "Shiro",
            Age:      22,
            Birthday: "1998/12/07",
        },
        "080-1111-2222",
        true,
    )
    adultUser.PrintMyData()
    adultUser.UpdateBaseData(entity.UserData{
        Name:     "白澤",
        Age:      23,
        Birthday: "1997/12/07",
    })
    adultUser.PrintMyData()
}
出力結果.
-------- Child --------
ようこそ!kuroさん(♂).あなたの誕生日は2001/12/07ですね!
ようこそ!黒澤さん(♂).あなたの誕生日は1998/03/07ですね!

-------- Adult --------
Shiro様のデータを user.json に保存しました.
白澤様のデータを user.json に保存しました.

今回は触れませんでしたが、インターフェースを定義するメリットの1つとして、テスタビリティが上がるというのもあります。

インターフェースは、実装してほしい関数の入力と出力の形だけを定めたものであるため、モック化してテストに利用することができます。

モック化とテストについてもいつか書きたいなと思っています!

まとめ

Go言語のサンプルコードを示しながら、抽象化することのメリットについて話してきました。

抽象化というと、日常で何気なく使っている、「ものごとの要素部分だけを抽出してわかりやすくする」という思考法を思い浮かべがちですが、プログラミングにおける抽象化はもっと幅広い意味を持っています。

皆さんの作っているアプリケーションのコードを見直してみてください。
もし、関数の中身が膨大になっていたり、1つの関数が異常に多くの責務を担当しているように感じたなら、抽象化の出番かもしれませんよ!!

ここまで読んでいただいたみなさん、ありがとうございました!
是非この後もCA21 Advent Calendar 2020をお楽しみください!!

みなさんにとって今年の締め括りが素晴らしいものになることを心から祈っています。ではっ:wave:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GO言語でカプセル化

以前に書いた記事 GO言語でクラスっぽいことをする の中で触れられなかった「カプセル化をGo言語で実現する方法」について書いていこうと思います。
ポリモフィズムに関しては今回も省略します。(いつか書きたい)

やりたいこと

  • コンストラクタでPrivate変数の初期値を設定する
  • Publicメソッドを外部から参照して実行する
  • Publicメソッド内でPrivate変数を利用する
  • Publicの定数を参照して利用する
  • Private変数の値を参照しようとするとエラーになることを確認する

全網羅じゃないけどよく使いそうな処理

実装例

main.go
package main

import (
    "fmt"

    "xxx.example.com/sample/human"
)

const line = "--------------------"

func main() {

    wiz := new(human.Human)
    wiz.Init("魔法少女", 10, 10)

    fmt.Println(human.Msg)

    fmt.Println(line)
    fmt.Println(wiz.Hello())
    fmt.Println(wiz.Attack())

    fmt.Println(line)
    human.Hoge()

    // wiz.hp undefined (cannot refer to unexported field or method hp)
    // wiz.hp = 100

    // wiz.name undefined (cannot refer to unexported field or method name)
    // fmt.Println(wiz.name)
}
human/human.go
package human

import "fmt"

const (
    // Msg ...
    Msg = "人類を作成してください..."
)

// Human ...
type Human struct {
    name string
    hp   int
    ap   int
}

// Init ...
func (h *Human) Init(name string, hp, ap int) {
    h.name = name
    h.hp = hp
    h.ap = ap
}

// Hello ...
func (h *Human) Hello() string {
    return fmt.Sprintf("こんにちは、私は%sです。", h.name)
}

// Attack ...
func (h *Human) Attack() string {
    return fmt.Sprintf("%sの攻撃!%dのダメージ!", h.name, h.ap)
}
human.hoge.go
package human

import "fmt"

// Hoge ...
func Hoge() {
    war := new(Human)
    war.Init("+‡†狂戦士†‡+", 10, 15)
    fmt.Println(war.Hello())
    fmt.Println(war.Attack())

    fmt.Println(war.name + "はフォルムチェンジした!")

    war.name = "俺TSUEEEEEEEE"
    war.ap = 100000
    fmt.Println(war.Attack())
}

実行結果

人類を作成してください...
--------------------
こんにちは、私は魔法少女です。
魔法少女の攻撃!10のダメージ!
--------------------
こんにちは、私は+‡†狂戦士†‡+です。
+‡†狂戦士†‡+の攻撃!15のダメージ!
+‡†狂戦士†‡+はフォルムチェンジした!
俺TSUEEEEEEEEの攻撃!100000のダメージ!

+‡†狂戦士†‡+はチーターであることが判明しました

実装内容の説明

前記事 GO言語でクラスっぽいことをする と被ってそうなところは省略します

外部からアクセス可能な要素

Go言語では「先頭が大文字の要素」は外部パッケージからもアクセスできます。
今回の例では const Msg type Human struct func Hello() などがそれに当たります。(他にもあります)
ここでは出てきませんが変数も指定可能です。

外部からアクセス不能な要素

Go言語では「先頭が小文字の要素」は外部パッケージからアクセスできません。
今回の例では Human.name などがそれに当たります。(他にもあります)
ここでは出てきませんが定数やメソッド、関数も指定可能です。

外部からアクセス不能な要素を指定した場合

// wiz.hp undefined (cannot refer to unexported field or method hp)
wiz.hp = 100

// wiz.name undefined (cannot refer to unexported field or method name)
fmt.Println(wiz.name)

そんな要素は存在しないものとしてコンパイルエラーになります。

同パッケージのファイルからは先頭小文字でもアクセス可能

war.name = "俺TSUEEEEEEEE"
war.ap = 100000

先頭小文字の要素であっても、同パッケージ内であれば好きに参照することができます。
+‡†狂戦士†‡+に好き勝手動かれたくない場合は別パッケージから使用するのがよいでしょう。

まとめ:Go言語でカプセル化するには

  • パッケージを分ける
  • 外部から参照されたくない要素は「先頭小文字」で定義する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のエラーパッケージ aerrors をつくった

Go言語のエラーパッケージ aerrors をつくりました。

モチベーション

  • 型っぽく使えるエラーがほしい
  • デフォルトで呼び出し元情報がほしい
  • 気軽にラップしたい
  • いい感じにログに書き出したい

使い方

基本は既存のパッケージと同じ用に使用することができます。

errors.New 風に

err := aerrors.New("new error")

fmt.Println(err)
// Output:
// new error

fmt.Errorf 風に

err := aerrors.Errorf("error: %d", 42)

fmt.Println(err)
// Output:
// error: 42

aerrors はスタンダードライブラリのエラーに加えてより詳細な情報を保持しています。
デフォルトでエラーレベルと呼び出し元の情報を保持しています。

err := aerrors.New("new error")

fmt.Printf("%+v", err)
// Output:
// new error:
//     priority: Error
//     callers: main.main:example/main.go:10

オプションでエラーレベルを変更することができます。

err := aerrors.New("new error", aerrors.Priority(aerrors.Info))

fmt.Printf("%+v", err)
// Output:
// new error:
//     priority: Info
//     callers: main.main:example/main.go:10

また、より詳細な情報を追加することも可能です。

err := aerrors.New("new error").WithValue(
  aerrors.String("foo", "Foo"),
  aerrors.Int("number", 42)
)

fmt.Printf("%+v", err)
// Output:
// new error:
//     priority: Error
//     callers: main.main:example/main.go:10
//     foo: Foo
//     number: 42

さらに、エラーを拡張して新たなエラーを作成することができます。
拡張したエラーは errors.Is 関数で真となるため、独自のエラー型のように扱うことも可能です。

appError := aerrors.New("app error")
err := appError.New("oops")

fmt.Println(err)
fmt.Printf("is parent: %v", errors.Is(err, appError))
fmt.Printf("parent: %v", err.Parent())
// Output:
// oops
// is parent: true
// parent: app error

fmt.Errorf と同様に既存のエラーをラップすることも可能です。

appError := appError := aerrors.New("application error")
err := appError.Errorf("error: %w", errors.New("oops"))

fmt.Println(err)
// Output:
// error: oops:
//     priority: Error
//     parent: application error
//     origin: oops
//     callers: main.main:example/main.go:12
//   - oops

詳しくは Godoc または ソース(Github) をご覧ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hugoで自作ブログサイトを作ってみる #1

※これは自作ブログに投稿したものと同じ記事です。
Hugoで自作ブログサイトを作ってみる

はじめに

最近、Twitter の有名なすごい人たちがみんな Hugo を使って自作ブログを作っていて、ほげーーやってみたいーー、ってなったので試してみた。
また、Go 製のツールということで、勉強したかった Go を使えるかも!という思いもあった。(結果、Go は 1 ミリも使わずに終わった)

さんぽしさん
Hugoでさくっと自作ブログを作った – さんぽしの散歩記

コミさん
Hello My New Blog

Hugo とは?

Hugo は Golang 製の静的サイトジェネレーター。HTML とか CSS みたいなものを自動生成してくれるやつ。
The world’s fastest framework for building websites | Hugo
そういえば昔、reveal.js っていう HTML/CSS で Web スライドが書けるやつを拡張した reveal-ck っていう Ruby 製のツールを使ったことがあった。
これもMarkdownでスライドを作れるっていうお手軽でいい感じだった。(途中からデザインを凝り始めて朝日が昇ってしまったが…)

話は戻って Hugo だが、めちゃくちゃ早いらしい。(他と比べたことがないからわからん)

何はともあれ構築していく。

雛形作成まで

Hugo にはたくさんの Thema があって、自由に選び放題。(ライセンスとかの確認は注意)
ここから好きに選べる。ブログサイト以外でも使えそうなものがたくさんだから、ちょっとしたハッカソンでのテンプレ作りに良さそう。(Hugo じゃなくてもいいけど)
Complete List | Hugo Themes

テンプレ作成

というわけで、まずは Hugo をインストールしていく。

$ brew install hugo

他の OS は本家を参照してください。

Install Hugo | Hugo

入ったら、次のコマンドで雛形が生成される

$ hugo new site blog && cd blog

$ hugo new site {好きな名前}で作られる

そうすると次のようなものたちが生成される

.
├── archetypes
│   └── default.md
├── config.toml
├── content
├── data
├── layouts
├── static
└── themes

現時点だと、全部空のディレクトリがあるだけ。

テンプレートテーマを適用していく

先ほどのサイトからテーマを選ぶ。
今回は、Timer Hugoというものを選んだ。レスポンシブに対応してるし、結構いろいろカスタムできそうだったから。

Timer Hugo | Hugo Themes

さっき生成した themas に submodule としてテーマを入れる

$ git init && git submodule add https://github.com/themefisher/timer-hugo.git themes/timer-hugo

このthemasには複数テンプレを入れてもいいが、そうなってくるとどれを適用するのかわからなくなるので、config.tomlで明示的に宣言しておく。

baseURL = "http://example.org/"
languageCode = "en-us"
title = "My New Hugo Site"
theme = "timer-hugo"

と、書いたが、実はこの config.toml も各テーマごとに固有の書き方がある。(カスタムされている)

ゆえにthemes/timer-hugo/exampleSite/config.tomlの内容をコピーした方がいい。

baseURL = "http://example.org/"
languageCode = "en-us"
title = "Timer | Responsive Multipurpose Bootstrap Hugo Template"
theme = "timer-hugo"

# We Used Ionicons Icon font for Icon, for more details check this link - https://ionicons.com/

# Navbar Menus
[[menu.main]]
name    = "About"
url     = "about"
weight  = 2
[[menu.main]]
name    = "Service"
url     = "service"
weight  = 3
[[menu.main]]
name    = "Gallery"
url     = "gallery"
weight  = 4
[[menu.main]]
name    = "Blog"
url     = "blog"
weight  = 5
[[menu.main]]
name    = "Contact"
url     = "contact"
weight  = 6

# Site Params
[params]
home = "Home"
logo = "images/logo.png"
dateFormat = "6 January 2006"
# Meta data
description = "Airspace Hugo theme"
author = "Themefisher"
# Google Analitycs
googleAnalitycsID = "Your ID"
# contact form action
contactFormAction = "#" # contact form works with https://formspree.io

# Banner Section
[params.banner]
enable  = true
bgImage = "images/slider.jpg"
heading = "HI, MY NAME IS JONATHON & I AM A"
description = "WITH 10 YEARS EXPERIENCE, I'VE OCCUPIED MANY ROLES INCLUDING DIGITAL DESIGN DIRECTOR, WEB DESIGNER AND DEVELOPER. THIS SITE SHOWCASES SOME OF MY WORK."
# button
btn     = true
btnText = "Download More"
btnURL  = "https://themefisher.com/"

# flip text
[[params.banner.flipText]]
title   = "DESIGNER"
[[params.banner.flipText]]
title   = "DEVELOPER"
[[params.banner.flipText]]
title   = "FATHER"

# Homepage About Section
[params.about]
enable  = true
title   = "ABOUT ME"
content = "Hello, I’m a UI/UX Designer & Front End Developer from Victoria, Australia. I hold a master degree of Web Design from the World University.And scrambled it to make a type specimen book. It has survived not only five centuries. <br> <br> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error, adipisci voluptatum repudiandae, natus impedit repellat aut officia illum at assumenda iusto reiciendis placeat. Temporibus, vero."
image   = "images/about/about.jpg"

# Call to Action
[params.cta]
enable  = true
title   = "SO WHAT YOU THINK ?"
content = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nobis,<br>possimus commodi, fugiat magnam temporibus vero magni recusandae? Dolore, maxime praesentium."
btnText = "Contact with me"
btnURL  = "/contact"

# Portfolio Section On Homepage
[params.portfolio]
enable  = true
title   = "Latest Works"
subtitle= "Aliquam lobortis. Maecenas vestibulum mollis diam. Pellentesque auctor neque nec urna. Nulla sit amet est. Aenean posuere tortor sed cursus feugiat, nunc augue blandit nunc, eu sollicitudin urna dolor sagittis lacus."

# social icon
[[params.socialIcon]]
icon = "ion-social-facebook"
url = "#"

[[params.socialIcon]]
icon = "ion-social-instagram"
url = "#"

[[params.socialIcon]]
icon = "ion-social-linkedin"
url = "#"

そのほかも、このテーマのいろいろなテンプレに従った方がいいので、themes/timer-hugo/exampleSiteにあるものを、雛形ホームのディレクトリに上書きコピーする。

加えて、themes/timer-hugo

  • archetypes
  • layouts
  • static

もコピーする。(自分はやらなかったが、assetes もコピーした方がいいかも)

これでサーバーを立ててみる。次のコマンドでできる。

$ hugo server

http://localhost:1313/blog にアクセスする。

demo と同じものが表示されているはず!万歳!
blog のところも見ると、テンプレで用意されている記事が見れる。

記事を書いてみる

次のコマンドで記事を作成する

$ hugo new blog/{記事のタイトルとか}.md

これでcontent/blog/{記事のタイトルとか}.mdに markdown が作られる。

この時作られるものは、archetypes/default.mdに設定されているものが自動生成される。ちなみに自分のdefault.mdはこのように設定している。

---
title: "{{ replace .Name "-" " " | title }}"
description : "This is meta description"
date: {{ .Date }}
draft: true # 反映させる時はfalseに変えるかコメントアウト
comments: true
adsense: false
archives: ["{{ dateFormat "2006" .Date }}", "{{ dateFormat "2006-01" .Date }}"]

# Twitter card gen用設定"]
author: ["いわし"]
categories: ["Test"]
tags: ["motivation", "inspiration"] # tag
ogimage: "images/og/{{ .Name }}.png" # tcardgenで生成した画像をOGP画像に設定する
url: "/{{ .Type }}/{{ .Name }}/" # tcardgenでの自動生成スクリプト用のパスを設定

# Blog用---------------------------------------------------
type: post
image: "images/og/{{ .Name }}.png" # ブログバナーの画像

# Portfolio用----------------------------------------------
caption: Product Mockup
image: images/portfolio/item-2.jpg
liveLink: link # ??
# 右側の情報説明
client: Julia Robertson
submitDate: November 20, 2017
category: ["mockup","design"] # tag
location: 1201 park street, Avenue, Dhaka

---

細かい設定等は次の記事とかで書いていきたいので今回は省く。
demo で入れられている markdown ファイルを参考にカスタマイズしていく。

基本はこんな感じ

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

気にするのはdraft: trueの部分。これをfalseに変えるかコメントアウトすると記事として適用される。このままだと表示されない。

変えたら次のコマンドを実行。

$ hugo

これでpublicディレクトリが作成されるはず。ヤッタネ。

github pages 公開する

これで markdown で記事を書いてから静的ファイルを生成するまでの流れはできた。

さて、ここからは今ローカルホストで表示しているものを github pages で公開したい。そのための準備をしていく。

ディレクトリ名を変更する

github pages はその仕様として、ホームディレクトリかdocsディレクトリを公開ディレクトリとして設定できる。

今のデフォルトだと、生成されるのはpublicなので、これをdocsに変える。

見るのはconfig.toml

baseURL = "https://biwashi.github.io/blog/" ####### 追加: {user_name}.github.io/{repository_name}/ を指定する
languageCode = "ja"
title = "MY NEW GEAR | IWASHI BLOG" # Homeのタイトル
theme = "timer-hugo"
publishDir = "docs"  ######################### 追加
canonifyurls = true  ######################## 追加(相対URLを絶対URLに変換できるようにする)

またこの時、baseURL を公開する github pages のリンクに設定する。

https://{githubのアカウント名}.github.io/{リポジトリ名}/

baseURL は、よくあるクローンするときの https のリンクじゃないので注意
実際に github pages で公開するときのリンク(これのせいで詰まった)
よく考えたら当たり前

また canonifyurls = true は相対パスに変更するために設定。

これで準備は完了。あとは push して公開ディレクトリをdocsに設定する。

master branch docs/ folder

しばらくすると公開される。やったね!

まとめ

これでサクッと雛形が作れました。テーマもお洒落なのがたくさんあるので見てるだけで楽しい!

次回はブログをカスタマイズしていくところを書きたい。(これが地獄の始まりだった…)

参考

参考させていただいた素晴らしい記事たちです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HugoとGithub Pagesで自作ブログサイトを作ってみる #1

※これは自作ブログに投稿したものと同じ記事です。
Hugoで自作ブログサイトを作ってみる

はじめに

最近、Twitter の有名なすごい人たちがみんな Hugo を使って自作ブログを作っていて、ほげーーやってみたいーー、ってなったので試してみた。
また、Go 製のツールということで、勉強したかった Go を使えるかも!という思いもあった。(結果、Go は 1 ミリも使わずに終わった)

さんぽしさん
Hugoでさくっと自作ブログを作った – さんぽしの散歩記

コミさん
Hello My New Blog

Hugo とは?

Hugo は Golang 製の静的サイトジェネレーター。HTML とか CSS みたいなものを自動生成してくれるやつ。
The world’s fastest framework for building websites | Hugo
そういえば昔、reveal.js っていう HTML/CSS で Web スライドが書けるやつを拡張した reveal-ck っていう Ruby 製のツールを使ったことがあった。
これもMarkdownでスライドを作れるっていうお手軽でいい感じだった。(途中からデザインを凝り始めて朝日が昇ってしまったが…)

話は戻って Hugo だが、めちゃくちゃ早いらしい。(他と比べたことがないからわからん)

何はともあれ構築していく。

雛形作成まで

Hugo にはたくさんの Thema があって、自由に選び放題。(ライセンスとかの確認は注意)
ここから好きに選べる。ブログサイト以外でも使えそうなものがたくさんだから、ちょっとしたハッカソンでのテンプレ作りに良さそう。(Hugo じゃなくてもいいけど)
Complete List | Hugo Themes

テンプレ作成

というわけで、まずは Hugo をインストールしていく。

$ brew install hugo

他の OS は本家を参照してください。

Install Hugo | Hugo

入ったら、次のコマンドで雛形が生成される

$ hugo new site blog && cd blog

$ hugo new site {好きな名前}で作られる

そうすると次のようなものたちが生成される

.
├── archetypes
│   └── default.md
├── config.toml
├── content
├── data
├── layouts
├── static
└── themes

現時点だと、全部空のディレクトリがあるだけ。

テンプレートテーマを適用していく

先ほどのサイトからテーマを選ぶ。
今回は、Timer Hugoというものを選んだ。レスポンシブに対応してるし、結構いろいろカスタムできそうだったから。

Timer Hugo | Hugo Themes

さっき生成した themas に submodule としてテーマを入れる

$ git init && git submodule add https://github.com/themefisher/timer-hugo.git themes/timer-hugo

このthemasには複数テンプレを入れてもいいが、そうなってくるとどれを適用するのかわからなくなるので、config.tomlで明示的に宣言しておく。

baseURL = "http://example.org/"
languageCode = "en-us"
title = "My New Hugo Site"
theme = "timer-hugo"

と、書いたが、実はこの config.toml も各テーマごとに固有の書き方がある。(カスタムされている)

ゆえにthemes/timer-hugo/exampleSite/config.tomlの内容をコピーした方がいい。

baseURL = "http://example.org/"
languageCode = "en-us"
title = "Timer | Responsive Multipurpose Bootstrap Hugo Template"
theme = "timer-hugo"

# We Used Ionicons Icon font for Icon, for more details check this link - https://ionicons.com/

# Navbar Menus
[[menu.main]]
name    = "About"
url     = "about"
weight  = 2
[[menu.main]]
name    = "Service"
url     = "service"
weight  = 3
[[menu.main]]
name    = "Gallery"
url     = "gallery"
weight  = 4
[[menu.main]]
name    = "Blog"
url     = "blog"
weight  = 5
[[menu.main]]
name    = "Contact"
url     = "contact"
weight  = 6

# Site Params
[params]
home = "Home"
logo = "images/logo.png"
dateFormat = "6 January 2006"
# Meta data
description = "Airspace Hugo theme"
author = "Themefisher"
# Google Analitycs
googleAnalitycsID = "Your ID"
# contact form action
contactFormAction = "#" # contact form works with https://formspree.io

# Banner Section
[params.banner]
enable  = true
bgImage = "images/slider.jpg"
heading = "HI, MY NAME IS JONATHON & I AM A"
description = "WITH 10 YEARS EXPERIENCE, I'VE OCCUPIED MANY ROLES INCLUDING DIGITAL DESIGN DIRECTOR, WEB DESIGNER AND DEVELOPER. THIS SITE SHOWCASES SOME OF MY WORK."
# button
btn     = true
btnText = "Download More"
btnURL  = "https://themefisher.com/"

# flip text
[[params.banner.flipText]]
title   = "DESIGNER"
[[params.banner.flipText]]
title   = "DEVELOPER"
[[params.banner.flipText]]
title   = "FATHER"

# Homepage About Section
[params.about]
enable  = true
title   = "ABOUT ME"
content = "Hello, I’m a UI/UX Designer & Front End Developer from Victoria, Australia. I hold a master degree of Web Design from the World University.And scrambled it to make a type specimen book. It has survived not only five centuries. <br> <br> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error, adipisci voluptatum repudiandae, natus impedit repellat aut officia illum at assumenda iusto reiciendis placeat. Temporibus, vero."
image   = "images/about/about.jpg"

# Call to Action
[params.cta]
enable  = true
title   = "SO WHAT YOU THINK ?"
content = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nobis,<br>possimus commodi, fugiat magnam temporibus vero magni recusandae? Dolore, maxime praesentium."
btnText = "Contact with me"
btnURL  = "/contact"

# Portfolio Section On Homepage
[params.portfolio]
enable  = true
title   = "Latest Works"
subtitle= "Aliquam lobortis. Maecenas vestibulum mollis diam. Pellentesque auctor neque nec urna. Nulla sit amet est. Aenean posuere tortor sed cursus feugiat, nunc augue blandit nunc, eu sollicitudin urna dolor sagittis lacus."

# social icon
[[params.socialIcon]]
icon = "ion-social-facebook"
url = "#"

[[params.socialIcon]]
icon = "ion-social-instagram"
url = "#"

[[params.socialIcon]]
icon = "ion-social-linkedin"
url = "#"

そのほかも、このテーマのいろいろなテンプレに従った方がいいので、themes/timer-hugo/exampleSiteにあるものを、雛形ホームのディレクトリに上書きコピーする。

加えて、themes/timer-hugo

  • archetypes
  • layouts
  • static

もコピーする。(自分はやらなかったが、assetes もコピーした方がいいかも)

これでサーバーを立ててみる。次のコマンドでできる。

$ hugo server

http://localhost:1313/blog にアクセスする。

demo と同じものが表示されているはず!万歳!
blog のところも見ると、テンプレで用意されている記事が見れる。

記事を書いてみる

次のコマンドで記事を作成する

$ hugo new blog/{記事のタイトルとか}.md

これでcontent/blog/{記事のタイトルとか}.mdに markdown が作られる。

この時作られるものは、archetypes/default.mdに設定されているものが自動生成される。ちなみに自分のdefault.mdはこのように設定している。

---
title: "{{ replace .Name "-" " " | title }}"
description : "This is meta description"
date: {{ .Date }}
draft: true # 反映させる時はfalseに変えるかコメントアウト
comments: true
adsense: false
archives: ["{{ dateFormat "2006" .Date }}", "{{ dateFormat "2006-01" .Date }}"]

# Twitter card gen用設定"]
author: ["いわし"]
categories: ["Test"]
tags: ["motivation", "inspiration"] # tag
ogimage: "images/og/{{ .Name }}.png" # tcardgenで生成した画像をOGP画像に設定する
url: "/{{ .Type }}/{{ .Name }}/" # tcardgenでの自動生成スクリプト用のパスを設定

# Blog用---------------------------------------------------
type: post
image: "images/og/{{ .Name }}.png" # ブログバナーの画像

# Portfolio用----------------------------------------------
caption: Product Mockup
image: images/portfolio/item-2.jpg
liveLink: link # ??
# 右側の情報説明
client: Julia Robertson
submitDate: November 20, 2017
category: ["mockup","design"] # tag
location: 1201 park street, Avenue, Dhaka

---

細かい設定等は次の記事とかで書いていきたいので今回は省く。
demo で入れられている markdown ファイルを参考にカスタマイズしていく。

基本はこんな感じ

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

気にするのはdraft: trueの部分。これをfalseに変えるかコメントアウトすると記事として適用される。このままだと表示されない。

変えたら次のコマンドを実行。

$ hugo

これでpublicディレクトリが作成されるはず。ヤッタネ。

github pages 公開する

これで markdown で記事を書いてから静的ファイルを生成するまでの流れはできた。

さて、ここからは今ローカルホストで表示しているものを github pages で公開したい。そのための準備をしていく。

ディレクトリ名を変更する

github pages はその仕様として、ホームディレクトリかdocsディレクトリを公開ディレクトリとして設定できる。

今のデフォルトだと、生成されるのはpublicなので、これをdocsに変える。

見るのはconfig.toml

baseURL = "https://biwashi.github.io/blog/" ####### 追加: {user_name}.github.io/{repository_name}/ を指定する
languageCode = "ja"
title = "MY NEW GEAR | IWASHI BLOG" # Homeのタイトル
theme = "timer-hugo"
publishDir = "docs"  ######################### 追加
canonifyurls = true  ######################## 追加(相対URLを絶対URLに変換できるようにする)

またこの時、baseURL を公開する github pages のリンクに設定する。

https://{githubのアカウント名}.github.io/{リポジトリ名}/

baseURL は、よくあるクローンするときの https のリンクじゃないので注意
実際に github pages で公開するときのリンク(これのせいで詰まった)
よく考えたら当たり前

また canonifyurls = true は相対パスに変更するために設定。

これで準備は完了。あとは push して公開ディレクトリをdocsに設定する。

master branch docs/ folder

しばらくすると公開される。やったね!

まとめ

これでサクッと雛形が作れました。テーマもお洒落なのがたくさんあるので見てるだけで楽しい!

次回はブログをカスタマイズしていくところを書きたい。(これが地獄の始まりだった…)

参考

参考させていただいた素晴らしい記事たちです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A Tour of Go メモ ~Flow control statements: for, if, else, switch and defer~

はじめに

Go の勉強のため、A Tour of Goに取り組んでいる
今回はFlow control statements: for, if, else, switch and defer章について、学んだことを記していく

使えそうなスニペット

※筆者は VSCode を使用

for

初期値、条件式、後処理、スコープ内部の順にカーソルが移動する

for i := 0; i < count; i++ {

}

if

条件式、スコープ内部にカーソルが移動する
el -> else { } で else ブロックを追加できる

if condition {

}

ei

一発でも作れる

if condition {

} else {

}

switch

条件式、case、スコープ内部にカーソルが移動する
cs -> case condition: で条件を追加できる

switch expression {
case condition:

}

df

defer func()

pn

panic("")

recover

こちらは存在しなかったので、自作した
(Preferences: Configure User Snippetsコマンドから作成可能)
出力されるのは、

defer func() {
        if r := recover(); r != nil {

        }
}()

User Snippet の Json は、

    "defer recover": {
        "prefix": "recover",
        "body": [
            "defer func() {",
            "    if r := recover(); r != nil {",
            "        $1",
            "    }",
            "}()",
        ],
        "description": "defer recover"
    }

ページごとの補足

For continued

for 文で定義した変数を、条件式で使わなくても良い

    x := 1
    for i := 0; 0 < x; {
        fmt.Println(i) // 流石にスコープ内で使わないと怒られる
    }

for で定義する変数は、定義済みでも良い

    x := 1
    for x = 0; x < 10; x++ {
        fmt.Println(x)
  }

If and else

  • if ステートメントで宣言された変数は、else ifブロック内でも使える
  • else ifステートメントで宣言された変数も、後続のelse ifelseブロック内でも使える
  • 同じ名前の変数を宣言した場合、上書きされる(=スコープがネストされる?)
    x := 1 // 1,2,3,4
    if y := 1; x == 1 {
        fmt.Println("x == 1 : y = ", y)
    } else if y := 2; x == 2 {
        fmt.Println("x == 2 : y = ", y)
    } else if y := 3; x == 3 {
        fmt.Println("x == 3 : y = ", y)
    } else {
        fmt.Println("x  > 3 : y = ", y)
    }
    // x == 1 : y = 1
    // x == 2 : y = 2
    // x == 3 : y = 3
    // x  > 3 : y = 3

Switch with no condition

Switch の case の条件式では変数宣言ができないため、例えば上述したif-elseは、置き換えることができなさそう

Defer

A Tour of Goでは言葉少なに紹介されているため、Defer, Panic, and Recover(日本語訳は こちら)の内容がメインになる

defer の使い方の 1 つは、他言語でいうfinally
(Python でいうwith、Kotlin でいうuseの代わりにもなる)

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

他にも panic, recover があり、それぞれ以下のように対応しそう

  • panic: throw error
  • recover: catch

以下はエラーハンドリングについて、公式のサンプルプログラム

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

これを実行すると、以下のようになる

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

少し癖がありますが、使い慣れると便利そう
何度もtry-catchを書かずに済みそうだ

雑感

このあたりは他言語と似通った仕様が多いので、難しい印象はない(最後の defer は別だが)
Go 言語は言語の仕様変更・追加が少ないので、数年前の記事が当てになるのが良い

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLでみるgormのAssociation

この記事は東京学芸大学 櫨山研究室 Advent Calendar 2020の7日目の記事になります.

初めての記事投稿で分かりづらいかもしれませんが, 暖かい目で見てほしいです.
変なところはコメントください!

はじめに

Goには素晴らしきORM(おーあーるまっぱー, SQLを知らなくてもDBとのやりとりができる!ってやつ)であるGORMが存在していて, 大変お世話になっています.

Association周りが全然わかっていないので, Debug()モードで発行されるSQLを確認していきます.

概要

gorm公式のAssociationに記述されているメソッドたちに, Debug()メソッドを用いて発行されるSQL文を確認していく.
今回は1対多のケースのみでやっています.

用意したモデル, テーブル

モデル

package models

type Writer struct {
    ID       int
    Name     string
    Articles []Article `gorm:"ForeignKey:WriterID"`
}

type Article struct {
    ID       int
    Title    string
    WriterID int `gorm:"column:writer_id"`
}

テーブル

CREATE TABLE `writers`(
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;


CREATE TABLE `articles`(
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
    `writer_id` int(10) unsigned,
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    FOREIGN KEY (`writer_id`) REFERENCES `writers`(`id`)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

関連は,
writer 1 --write-- * article
となっています.

Create

実行するコード

newWriter := models.Writer{
        Name: "new name",
        Articles: []models.Article{
            {
                Title: "新しい記事の形",
            },
            {
                Title: "新版!元気になる高笑い",
            },
        },
    }
    db.Debug().Create(&newWriter)
    fmt.Printf("result: %+v\n", newWriter)

実行結果

[2020-12-06 16:59:22]  [3.64ms]  INSERT INTO `writers` (`name`) VALUES ('new name')  
[1 rows affected or returned ] 

[2020-12-06 16:59:22]  [6.34ms]  INSERT INTO `articles` (`title`,`writer_id`) VALUES ('新しい記事の形',8)  
[1 rows affected or returned ] 

[2020-12-06 16:59:22]  [7.60ms]  INSERT INTO `articles` (`title`,`writer_id`) VALUES ('新版!元気になる高笑い',8)  
[1 rows affected or returned ] 
result: {ID:8 Name:new name Articles:[{ID:12 Title:新しい記事の形 WriterID:8} {ID:13 Title:新版!元気になる高笑い WriterID:8}] Likes:[]}

DBに登録させていない記事を構造体に持たせると新しい子レコードを作成してくれる.
Omit()を用いれば, Articlesを保存しなくて済むそう.

    newWriter := models.Writer{
        Name: "new name2",
        Articles: []models.Article{
            {
                Title: "本を書いてみる",
            },
            {
                Title: "新作の本B",
            },
        },
    }
    db.Debug().Omit("Articles").Create(&newWriter)
    fmt.Printf("result: %+v\n", newWriter)

実行結果

2020-12-06 22:30:45]  [3.75ms]  INSERT INTO `writers` (`name`) VALUES ('new name2')  
[1 rows affected or returned ] 
result: {ID:8 Name:new name2 Articles:[{ID:0 Title:本を書いてみる WriterID:0} {ID:0 Title:新作の本B WriterID:0}] Likes:[]}

新しいArticleが保存されていない!

Association Find

子要素を持ってこれるらしい

    writer := models.Writer{
        ID: 1,
    }
    var articles []models.Article
    db.Debug().Model(&writer).Association("Articles").Find(&articles)

実行結果

[2020-12-06 22:47:20]  [3.95ms]  SELECT * FROM `articles`  WHERE (`writer_id` = 1)  
[1 rows affected or returned ] 

Append Association

実行するコード

子要素を追加することができる.
下記のコードの場合, IDが1番のwriterにarticle10という名前のarticleが追加される

    writer := models.Writer{
        ID: 1,
    }
    articles := []models.Article{
        {
            Title: "article10",
        },
    }
    db.Debug().Model(&writer).Association("Articles").Append(articles)

実行結果


[2020-12-06 23:13:28]  [3.32ms]  INSERT INTO `articles` (`title`,`writer_id`) VALUES ('article10',1)  
[1 rows affected or returned ] 

[2020-12-06 23:13:28]  [5.35ms]  SELECT * FROM `writers`  WHERE `writers`.`id` = 1 ORDER BY `writers`.`id` ASC LIMIT 1  
[1 rows affected or returned ] 

Replace Associations

実行するコード

    writer := models.Writer{
        ID: 1,
    }
    articles := []models.Article{
        {
            ID:    3,
            Title: "article10",
        },
        {
            ID:    4,
            Title: "article1010",
        },
    }
    db.Debug().Model(&writer).Association("Articles").Replace(&articles)

実行結果


[2020-12-06 23:48:38]  [4.24ms]  UPDATE `articles` SET `title` = 'article10', `writer_id` = 1  WHERE `articles`.`id` = 3  
[1 rows affected or returned ] 

[2020-12-06 23:48:38]  [7.74ms]  SELECT * FROM `writers`  WHERE `writers`.`id` = 1 ORDER BY `writers`.`id` ASC LIMIT 1  
[1 rows affected or returned ] 

[2020-12-06 23:48:38]  [4.29ms]  UPDATE `articles` SET `title` = 'article10', `writer_id` = 1  WHERE `articles`.`id` = 3  
[0 rows affected or returned ] 

[2020-12-06 23:48:38]  [6.09ms]  SELECT * FROM `articles`  WHERE `articles`.`id` = 3 ORDER BY `articles`.`id` ASC LIMIT 1  
[1 rows affected or returned ] 

[2020-12-06 23:48:38]  [10.70ms]  UPDATE `articles` SET `title` = 'article1010', `writer_id` = 1  WHERE `articles`.`id` = 4  
[0 rows affected or returned ] 

[2020-12-06 23:48:38]  [10.22ms]  SELECT * FROM `articles`  WHERE `articles`.`id` = 4 ORDER BY `articles`.`id` ASC LIMIT 1  
[0 rows affected or returned ] 

[2020-12-06 23:48:38]  [23.08ms]  INSERT INTO `articles` (`id`,`title`,`writer_id`) VALUES (4,'article1010',1)  
[1 rows affected or returned ] 

[2020-12-06 23:48:38]  [3.73ms]  SELECT * FROM `writers`  WHERE `writers`.`id` = 1 ORDER BY `writers`.`id` ASC LIMIT 1  
[1 rows affected or returned ] 

[2020-12-06 23:48:38]  [4.34ms]  UPDATE `articles` SET `writer_id` = NULL  WHERE (`id` NOT IN (3,4)) AND (`writer_id` = 1)  
[0 rows affected or returned ] 

Replaceするときは, 新しく子要素に追加したもの以外は, 外部キーとなるカラムの値がNULLになるっぽい.

Delete Association

実行するコード

    writer := models.Writer{
        ID: 1,
    }
    articles := []models.Article{
        {
            ID:    3,
            Title: "article10",
        },
        {
            ID:    4,
            Title: "article1010",
        },
    }
    db.Debug().Model(&writer).Association("Articles").Delete(&articles)

実行結果

[2020-12-06 23:56:37]  [4.91ms]  UPDATE `articles` SET `writer_id` = NULL  WHERE (`writer_id` IN (1)) AND (`id` IN (3,4))  
[0 rows affected or returned ] 

指定した子要素のarticleのwriter_id(外部キー)がNULLになるようにSQLが発行されている.

Clear Association

実行するコード

    writer := models.Writer{
        ID: 1,
    }
    db.Debug().Model(&writer).Association("Articles").Clear()

実行結果

[2020-12-07 00:01:36]  [4.39ms]  UPDATE `articles` SET `writer_id` = NULL  WHERE (`writer_id` = 1)  
[0 rows affected or returned ] 

Delete Associationよりも強力で子要素を全部消してるらしい.

おわりに

Associationモードで発行されるSQL文をまとめてみました.
1対多でしか検証ができていないので, いつかMany2Manyのケースでも試してみたいです.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerを使ってPostgreSQL+Goの開発環境を作ってみた

はじめに

GoからPostgreSQLに接続する部分がよくわかってなかったので、自分でDocker環境作って試してみた。

参考

ここを大いに参考にさせてもらった。
Go PostgreSQLにつないでみる - Qiita
Docker で作る postgres 環境 | Crudzoo

Docker

ポイントはnetworks部分を同じにしてるところ。

ネットワークの準備

# docker network create postgres-test-network
# docker network ls
NETWORK ID          NAME                    DRIVER              SCOPE
xxxxxxxxxxxx        postgres-test-network   bridge              local

PostgreSQL

こんな感じ。
POSTGRES_HOST_AUTH_METHOD: trueは推奨してないって言われたけど、ローカルのお試し環境だから無視した。

Dockerfile
FROM postgres:11-alpine
ENV LANG ja_JP.utf8
docker-compose.yml
version: '3'
services:
  db:
    build: .
    tty: true
    ports:
      - 5434:5432
    environment:
      POSTGRES_USER: root
      POSTGRES_HOST_AUTH_METHOD: trust
networks:
  default:
    external:
      name: postgres-test-network

これで立ち上げる

docker-compose up -d

Go

Dockerfile
FROM golang:latest
RUN mkdir /go/src/work
WORKDIR /go/src/work
ADD . /go/src/work
docker-compose.yml
version: '3'
services:
  app:
    build: .
    tty: true
    volumes:
      - .:/go/src/work
networks:
  default:
    external:
      name: postgres-test-network

これで立ち上げる

docker-compose up -d

アクセス部分

Goの方のコンテナの中では、これを実行する。

ここをほぼほぼコピペさせてもらった。
Go PostgreSQLにつないでみる - Qiita

main.go
package main

import (
    "database/sql"
    "fmt"

    _ "github.com/lib/pq"
)

type EMPLOYEE struct {
    ID     string
    NUMBER string
}

func main() {
    db, err := sql.Open("postgres", "host=db port=5432 user=root sslmode=disable")
    defer db.Close()

    if err != nil {
        fmt.Println(err)
    }

    if _, err := db.Exec("CREATE TABLE employee (emp_id serial PRIMARY KEY, emp_number INTEGER);"); err != nil {
        fmt.Println(err)
    }

    // INSERT
    var empID string
    id := 4
    number := 4445
    err = db.QueryRow("INSERT INTO employee (emp_id, emp_number) VALUES($1,$2) RETURNING emp_id", id, number).Scan(&empID)

    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(empID)

    // SELECT
    rows, err := db.Query("SELECT * FROM employee")

    if err != nil {
        fmt.Println(err)
    }

    var es []EMPLOYEE
    for rows.Next() {
        var e EMPLOYEE
        rows.Scan(&e.ID, &e.NUMBER)
        es = append(es, e)
    }
    fmt.Printf("%v", es)
}

lib/pqが入ってなかったので、go getした。ドライバーらしい。

go get github.com/lib/pq

いろいろ試してるとき、ネットワークつながらないって何回か言われたので、pingして確認した。
Dockerがうまいこと、DNSサーバーたててcomposeにかいたservicesの名前で登録してくれるらしい。

# ping db
PING db (172.20.0.3) 56(84) bytes of data.
64 bytes from work2_postgres_db_1.postgres-test-network (172.20.0.3): icmp_seq=1 ttl=64 time=0.258 ms

これで。実行したら、それっぽく動いた。

go run main.go

Posticoで確認

一応、DBの中身もGUIツールで確認した。良い感じ。
Postico – a modern PostgreSQL client for the Mac
image.png

Screen Shot 2020-12-07 at 8.41.10.png

おわりに

ローカルでに動く環境作れたので、いろいろ試してみたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】Channelを使ってSingle Flightなキャッシュを実装する

昨年のAdvent Calendarに引き続きChannelネタです。

前置き

データベースアクセスを削減する為に取得したデータをローカルにキャッシュする、みたいなことをよくやります。

シンプルに書くと↓みたいなカンジでしょうか。

var cache sync.Map

func get(key string) (v interface{}, err error) {
    v, ok := cache.Load(key)
    if !ok {
        v, err = getFromDB(key)
        if err != nil {
            return nil, err
        }
        cache.Store(key, v)
    }
    return v, nil
}

キャッシュが存在しなければDBに取得しに行きます。

さて、上記コード、一つ問題があります。
キャッシュされていない状態で同じkeyでcache.Load→cache.Storeの間にアクセスが集中すると、それらは全てデータベースに同じデータを取得しに行ってしまい、無駄が多いです。

Untitled.png1

そこで、誰かがデータを取得しに行ってる間、同じデータが必要な後続リクエストはそれを待機する様にしたいと思います。

Untitled(1).png1

作戦

先発リクエストデータ取得中の後続リクエストの待機にChannelを利用します。
Channelはキャッシュのエントリ毎に保持します。

  1. 先発リクエストはまず空のキャッシュエントリを作成(予約)しChannelを保存する
  2. 先発リクエストがデータ取得開始
  3. 後続リクエストは1で作成されたChannelをread。先発リクエストのデータ取得を待機
  4. 先発リクエストはデータを取得したらエントリに格納し、Channelをclose
  5. 待機していた後続リクエストが動き出しデータを参照

Channelは結構サイズ(メモリ使用量)が大きいので節約する為にキャッシュ完了したらnilをセットしておきます。

実装

キャッシュエントリ

type entry struct {
    lock  chan struct{} // lock for fetch
    value interface{}
    err   error
}

func (ce *entry) getWithTimeout(dst interface{}, timeout time.Duration) (interface{}, error) {
    if lock := ce.lock; lock != nil { // nil lock means cache is ready
        if timeout < 0 { // no timeout
            <-ce.lock
        } else {
            select {
            case <-lock:
            case <-time.After(timeout):
                return nil, ErrGetCacheTimeout
            }
        }
    }

    if ce.err != nil {
        return nil, ce.err
    }

    return ce.value, nil
}

エントリのデータを参照する前にかならずChannelをreadします。
データが格納されるまではブロックされます。
データが格納されたらChannelがcloseされるので、それからデータを参照します。

引数でタイムアウトを指定できる様にしました。-1ならば無制限、0ならばTry-Lock(ロックされていたら待機せず処理続行)の意味合いとなります。

キャッシュ本体

type Cache struct {
    cache sync.Map
}

func (c *Cache) Get(key interface{}) (value interface{}, err error) {
    return c.GetWithTimeout(key, -1)
}

func (c *Cache) GetWithTimeout(key interface{}, timeout time.Duration) (value interface{}, err error) {
    e, ok := c.cache.Load(key)
    if !ok || e == nil {
        return nil, ErrEntryNotFound
    }
    return e.(*entry).getWithTimeout(key, timeout)
}

type ResolveFunc func(entity interface{}, err error)

func (c *Cache) Reserve(key interface{}) ResolveFunc {
    entry := &entry{lock: make(chan struct{})}

    resolve := func(entity interface{}, err error) {
        entry.value, entry.err = entity, err
        close(entry.lock)
        entry.lock = nil // set nil to save memory
    }

    c.cache.Store(key, entry)

    return resolve
}

利用方法

cache := new(Cache) // 生成

value, err := cache.Get(key)
if err == ErrEntryNotFound {
    resolve := cache.Reserve(key) // エントリ予約

    value, err = getFromDB(key) // DB参照

    resolve(value, err) // キャッシュ保存

    if err != nil {
        return err
    }
}

厳密には cache.Get(key) から cache.Reserve(key) の間にアクセスがあると重複してDBにフェッチが走ってしまいますが、この間隔は極小の為それほど問題にはならないかと思います。気になる場合はMutexで排他してもよいでしょう。

弱点(デメリット)

とにかくChannelのサイズが大きい!!?

メモリアロケーションを計測しましたが、Channel1個辺り約1KiBくらいは消費します。
キャッシュしたらnilを設定して解放してるのでそれほど問題にはならないかと思いますが、同時に大量キーにアクセスされると瞬間的なメモリ使用量は大きくなる可能性があります。

Channelの代わりにsync.Mutex(or sync.RWMutex)を利用することも可能です。サイズは半分以下になります。
ただしMutexはタイムアウトやTry-Lockが実装出来ない(よね?)という制限があります。それら機能が不要ならばMutexを使うのもアリかと思います。

Channel, Mutex以外にもよい方法をご存知の方はコメントで教えて頂けるととても嬉しいです?‍♂️

作った

本記事内容のライブラリを作成しました^^

https://github.com/knightso/kocache

有効期限指定や、統計機能なども追加してあります。
内部のキャッシュ実装はHashiCorpさんのgolang-lruをそのまま利用させてもらったのでサイズ上限も指定可能です。

使い方はREADMEやGodocを参照ください。


  1. 本記事内で使用しているGopher画像はRenée Frenchさんのデザインを加工したものです 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GolangでのORM使ったDBアクセスロジックのtestcode事例(Functional Option Patternを使ったテストデータ準備関数を用意)

お題

表題の通り。※使用するORMはSQL Boilerだけど、他のORM(例えばGorm)でも適用方法は同じだと思う。

指定のテーブルから複数レコードを取得するような関数があったとして、そのtestcodeの事例。
例えば以下のようなコードでレコードを登録しておく。

func TestCustomers(t *testing.T) {
    〜〜省略〜〜
    c := repository.Customer{
        ID:        1,
        FirstName: "Satoru",
        LastName:  "Sato",
        Age:       30,
        Nickname:  null.StringFrom("toru"),
        Memo:      null.StringFrom("メモ"),
    }
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        t.Fatal(err)
    }
    〜〜省略〜〜
    // test
}

テストケースで必要な分だけ、この手のコードを書いていく必要があってツラい。
それに、ケースによっては大抵のカラムは固定値でよくって指定のカラムだけバリエーションを持たせたいこともある。
当然、単に以下のように生成関数に逃しただけでは他のテストケースで使う時に使えないケースが多々でてきてダメ。

func CreateCustomer(ctx context.Context, db *sqlx.DB) error {
    c := repository.Customer{
        ID:        1,
        FirstName: "Satoru",
        LastName:  "Sato",
        Age:       30,
        Nickname:  null.StringFrom("toru"),
        Memo:      null.StringFrom("メモ"),
    }
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        return err
    }
    return nil
}

じゃあ、どのようなケースでも対応できるように、設定できるカラムは全て引数に持たせてみると、

func CreateCustomer(ctx context.Context, db *sqlx.DB, id int64, firstName, lastName string, age int, nickname, memo string) error {
    c := repository.Customer{
        ID:        id,
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
        Nickname:  null.StringFrom(nickname),
        Memo:      null.StringFrom(memo),
    }
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        return err
    }
    return nil
}

ないし、

func CreateCustomer(ctx context.Context, db *sqlx.DB, c repository.Customer) error {
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        return err
    }
    return nil
}

みたいになって、結局、呼び出し元の負荷が変わってないのでは?となってしまう。
「じゃあ、とりあえず、外からパラメータ渡したいカラムだけ引数にしようか」と思い、以下のように「ID」と「名前」だけパラメータにすると、

func CreateCustomer(ctx context.Context, db *sqlx.DB, id int64, firstName, lastName string) error {
    c := repository.Customer{
        ID:        id,
        FirstName: firstName,
        LastName:  lastName,
        Age:       30,
        Nickname:  null.StringFrom("toru"),
        Memo:      null.StringFrom("メモ"),
    }
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        return err
    }
    return nil
}

あとから別のテストケースを書く時に「あっ、Ageが10代と20代の時とで挙動が違うケースのテストデータ準備したいからAgeもパラメータに追加したい!」となり、関数のI/Fが変わり、呼び元すべて修正という羽目になる。

とりあえず、今あるテストケースで必要なカラムの分だけパラメータにしておきつつも、あとから呼び出し元を変えることなく必要に応じてパラメータを追加したい。
というわけで、Functional Option Pattern(※Go使いには有名なパターンだと思うので特に説明は無しで)を使ってみる。

すると、以下のようになる。

func CreateCustomer(ctx context.Context, db *sqlx.DB, opts ...CustomerOption) error {
    c := repository.Customer{
        FirstName: "ダミー名",
        LastName:  "ダミー姓",
        Age:       99,
    }
    for _, o := range opts {
        o(&c)
    }
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        return err
    }
    return nil
}

goは可変長引数が使える。なので opts ...CustomerOption のようにオプションとして上書きしたいパラメータを1つの共通の型で定義してやり、それを0〜任意の数だけ渡せる。
大事なのは、0(つまり渡さなくてもいい)がOKだと言うこと。
CustomerOptionについては後述。

    c := repository.Customer{
        FirstName: "ダミー名",
        LastName:  "ダミー姓",
        Age:       99,
    }

で、NOT NULLカラムにだけデフォルト値を設定しておく。(※IDはPKでありPostgreSQLからシリアル採番させる。)
そして、そのあとの↓によって、渡されたパラメータの分だけ、カラムを上書きしていく。

    for _, o := range opts {
        o(&c)
    }

これも、ここだけ見てても「何のこっちゃ?」な気がするので、そろそろ CustomerOption の定義を載せることにする。

type CustomerOption func(*repository.Customer)

関数型にしている。つまり、 for _, o := range opts {opts は関数が複数ループしてるということ。
なので、o(&c) なんてコードになる。(渡した複数の関数の1つ1つに Customer構造体の参照を引数として渡している。)
じゃあ、CustomerOptionって具体的に何が渡ってくるのかと言うと、たとえば以下。

func withFirstName(firstName string) CustomerOption {
    return func(c *repository.Customer) {
        c.FirstName = firstName
    }
}

これを渡すと、いざ CreateCustomer(~~) 関数を実行した時に、CustomerFirstNameがデフォルトの「"ダミー名"」から、firstNameとして渡した名前に変わる。
CustomerOptionは可変長引数でいくつでも渡せるようになっているので、LastNameもデフォルトから変えたいと思った時は以下も渡せばいい。

func withLastName(lastName string) CustomerOption {
    return func(c *repository.Customer) {
        c.LastName = lastName
    }
}

これにより、もし、対象のテーブルにカラムが増えて、それをテストケースでパラメータを渡したいとなった時も、上記のように withXXXX(~~) を追加実装して、必要なテストケースでだけ使えばいい。

ちなみに、実際のテストケースではこのように使う。

// オプション未指定のデフォルトCustomerを生成(1レコード目なのでIDは 1 になるはず)
if err := CreateCustomer(ctx, db); err != nil {
    t.Fatal(err)
}

// 全てのパラメータを上書きしてCustomerを生成
if err := CreateCustomer(ctx, db,
    withID(2),
    withFirstName("Satoru"),
    withLastName("Sato"),
    withAge(30),
    withNickname("toru"),
    withMemo("メモ")); err != nil {
    t.Fatal(err)
}

以上で、今回の主題としては書ききってしまった。。。

前提

以下は済んだ上での作業。

  • Golangのローカルでの開発環境構築

開発環境

# OS

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"

# Golang

$ go version
go version go1.15.2 linux/amd64

# IDE(Goland)

GoLand 2020.3
Build #GO-203.5981.98, built on November 25, 2020

# Database

postgres:13

# DB管理環境(DataGrip)

DataGrip 2020.3
Build #DB-203.5981.102, built on November 25, 2020

実践

ソース全量は下記。
https://github.com/sky0621/tips-go/tree/v0.2.0/try/ormtest

言いたきことは「お題」ですべて言ってしまったので、以降は、ひたすら書いたソースを記載するだけ。。。

tree

$ tree
.
├── db
│   ├── dbconfig.yml
│   ├── docker-compose.yml
│   ├── insert.sql
│   ├── local
│   │   ├── data
│   │   └── testdata
│   └── migration
│       └── 20201206143541-create-table.sql
├── scripts
│   ├── sql-migrate-new.sh
│   ├── sql-migrate-up.sh
│   └── sqlboiler.sh
└── src
    ├── adapter
    │   ├── customer.go
    │   ├── customer_test.go
    │   └── main_test.go
    ├── cmd
    │   └── main.go
    ├── customer.go
    ├── go.mod
    ├── go.sum
    ├── repository
    │   ├── boil_main_test.go
    │   ├── boil_queries.go
    │   ├── boil_queries_test.go
    │   ├── boil_suites_test.go
    │   ├── boil_table_names.go
    │   ├── boil_types.go
    │   ├── customer.go
    │   ├── customer_test.go
    │   ├── psql_main_test.go
    │   ├── psql_suites_test.go
    │   └── psql_upsert.go
    └── sqlboiler.toml

DB

docker-composeで立ち上げる。
普通にアプリ起動した時につなぐDBとテストコードからアクセスするDBは分けてる。
(じゃないと、アプリ起動して動作確認してる時とテストコード流してる時とでデータが混ざる。そしてテスト結果が変わりうる。)

db/docker-compose.yml
version: '3'

services:
  db:
    restart: always
    image: postgres:13-alpine
    container_name: tips-go-try-ormtest-db-postgres-container
    ports:
      - "22456:5432"
    environment:
      - DATABASE_HOST=localhost
      - POSTGRES_DB=tips-go-try-ormtest-db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=yuckyjuice
      - PGPASSWORD=yuckyjuice
    volumes:
      - ./local/data:/docker-entrypoint-initdb.d/

  testdb:
    restart: always
    image: postgres:13-alpine
    container_name: tips-go-try-ormtest-testdb-postgres-container
    ports:
      - "33456:5432"
    environment:
      - DATABASE_HOST=localhost
      - POSTGRES_DB=tips-go-try-ormtest-testdb
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=yuckyjuice
      - PGPASSWORD=yuckyjuice
    volumes:
      - ./local/testdata:/docker-entrypoint-initdb.d/

テーブル作成

Go製のsql-migrateを使ってマイグレーション。
マイグレーションファイルは下記。

db/migration/20201206143541-create-table.sql
-- +migrate Up
CREATE TABLE customer (
  id bigserial NOT NULL,
  first_name varchar(32) NOT NULL,
  last_name varchar(32) NOT NULL,
  age int NOT NULL,
  nickname varchar(64),
  memo text,
  PRIMARY KEY (id)
);

-- +migrate Down
DROP TABLE customer;

マイグレーション実行用のシェルは下記。

scripts/sql-migrate-up.sh
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR=$(dirname "$0")
echo "${SCRIPT_DIR}"
cd "${SCRIPT_DIR}" && cd ../db

# https://github.com/rubenv/sql-migrate
#go get -v github.com/rubenv/sql-migrate/...
sql-migrate up -env="local"
sql-migrate up -env="localtest"

上記の -env= にて指定された "local""localtest"は何かと言うと、以下で定義した設定。

db/dbconfig.yml
local:
  dialect: postgres
  datasource: host=localhost port=22456 dbname=tips-go-try-ormtest-db user=postgres password=yuckyjuice sslmode=disable
  dir: migration
  table: migration
localtest:
  dialect: postgres
  datasource: host=localhost port=33456 dbname=tips-go-try-ormtest-testdb user=postgres password=yuckyjuice sslmode=disable
  dir: migration
  table: migration

アプリ起動用とテスト用の2つのDBそれぞれにマイグレーションを流す(つまり、customerテーブルを作る)ということ。

ORマッパー

今回使っているSQL Boilerは、既にDBにテーブルが存在する場合、そこからテーブル・カラム情報を拾って、自動でGo用の構造体やアクセスロジックを生成してくれる。

以下のようなTOMLファイルを書いて、

src/sqlboiler.toml
output   = "repository"
pkgname  = "repository"

[psql]
  host   = "localhost"
  port   = 22456
  dbname = "tips-go-try-ormtest-db"
  user   = "postgres"
  pass   = "yuckyjuice"
  sslmode= "disable"
  blacklist = [
    "migration",
  ]

以下のシェルから自動生成コマンドを叩くと、

scripts/sqlboiler.sh
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR=$(dirname "$0")
echo "${SCRIPT_DIR}"
cd "${SCRIPT_DIR}" && cd ../src

rm -rf ./repository/*

sqlboiler --wipe psql

以下のように各種Goファイルが自動生成される。(各ソースの説明は割愛)

└── src
    ├── repository
    │   ├── boil_main_test.go
    │   ├── boil_queries.go
    │   ├── boil_queries_test.go
    │   ├── boil_suites_test.go
    │   ├── boil_table_names.go
    │   ├── boil_types.go
    │   ├── customer.go
    │   ├── customer_test.go
    │   ├── psql_main_test.go
    │   ├── psql_suites_test.go
    │   └── psql_upsert.go

アプリケーションコード

main関数

src/cmd/main.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/sky0621/tips-go/try/ormtest/src/adapter"

    "github.com/go-chi/chi"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

func main() {
    // MEMO: ローカルでしか使わないので、ベタ書き
    dsn := "host=localhost port=22456 dbname=tips-go-try-ormtest-db user=postgres password=yuckyjuice sslmode=disable"
    db, err := sqlx.Connect("postgres", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if db != nil {
            // It is rare to Close a DB, as the DB handle is meant to be long-lived and shared between many goroutines.
            if err := db.Close(); err != nil {
                log.Fatal(err)
            }
        }
    }()

    customerService := adapter.NewCustomerService(db)

    r := chi.NewRouter()
    r.HandleFunc("/customers", func(w http.ResponseWriter, r *http.Request) {
        customers, err := customerService.Customers(r.Context())
        if err != nil {
            if _, err := w.Write([]byte(err.Error())); err != nil {
                log.Fatal(err)
            }
        }
        for _, c := range customers {
            if _, err := w.Write([]byte(fmt.Sprintf("%#+v\n", c))); err != nil {
                log.Fatal(err)
            }
        }
    })

    if err := http.ListenAndServe(":8080", r); err != nil {
        panic(err)
    }
}

DBアクセス用の構造体を準備して、それをサービス(CustomerService)に渡す。
アプリはWebAPIを提供するサーバとして起動する。現状提供しているのは「"/customers"」というパスにアクセスした時に全カスタマー情報をレスポンスに書き込む機能だけ。
※今回の主題と関係ないのでレスポンスはJSON形式ですらない。アクセスしても以下のようにそっけなく構造体の中身がブラウザに表示されるだけ。
Screenshot at 2020-12-07 01-14-06.png

カスタマーサービスロジック

インタフェースは下記。

src/customer.go
package ormtest

import (
    "context"
)

type CustomerService interface {
    Customers(context.Context) ([]*Customer, error)
}

type Customer struct {
    ID       int64  `json:"id"`
    FullName string `json:"fullName"`
    Age      int    `json:"age"`
    Nickname string `json:"nickname,omitempty"`
    Memo     string `json:"memo,omitempty"`
}

実装は下記。今回の主題では、この Customers(ctx context.Context) ([]*ormtest.Customer, error) がテスト対象。
単にDBから全customerレコードを取得して指定の構造体に変換して返すだけ。
SQL Boilerを使っているので実際にDBにアクセスするコードは models, err := repository.Customers().All(ctx, a.db) これだけ。

src/adapter/customer.go
package adapter

import (
    "context"
    "fmt"

    "github.com/sky0621/tips-go/try/ormtest/src/repository"

    "github.com/jmoiron/sqlx"

    ormtest "github.com/sky0621/tips-go/try/ormtest/src"
)

func NewCustomerService(db *sqlx.DB) ormtest.CustomerService {
    return &customerAdapter{db}
}

type customerAdapter struct {
    db *sqlx.DB
}

func (a *customerAdapter) Customers(ctx context.Context) ([]*ormtest.Customer, error) {
    models, err := repository.Customers().All(ctx, a.db)
    if err != nil {
        return nil, err
    }
    results := []*ormtest.Customer{}
    for _, model := range models {
        results = append(results, &ormtest.Customer{
            ID:       model.ID,
            FullName: fmt.Sprintf("%s %s", model.LastName, model.FirstName),
            Age:      model.Age,
            Nickname: model.Nickname.String,
            Memo:     model.Memo.String,
        })
    }
    return results, nil
}

テストコード

パッケージテスト用コード

テスト用DB接続や後始末(対象テーブルデータのtruncate等)用の関数も用意。

src/adapter/main_test.go
package adapter

import (
    "fmt"
    "log"
    "testing"

    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

func TestMain(m *testing.M) {
    db := setupDB()
    defer teardownDB(db)

    m.Run() // go v1.15 からは os.Exit 不要らしい
}

func setupDB() *sqlx.DB {
    // MEMO: ローカルでしか使わないので、ベタ書き
    dsn := "host=localhost port=33456 dbname=tips-go-try-ormtest-testdb user=postgres password=yuckyjuice sslmode=disable"
    db, err := sqlx.Connect("postgres", dsn)
    if err != nil {
        log.Fatal(err)
    }
    return db
}

func teardownDB(db *sqlx.DB) {
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()
    rows, err := db.Queryx(`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename != 'migration'`)
    if err != nil {
        log.Println(err)
        return
    }
    for rows.Next() {
        var tableName string
        if err := rows.Scan(&tableName); err != nil {
            log.Println(err)
            return
        }
        // RESTART IDENTITY ... 消去されるテーブルの列により所有されるシーケンスを自動的に再起動させます。
        // CASCADE ... 指定されたテーブル、または、CASCADEにより削除対象テーブルとされたテーブルを参照する外部キーを持つテーブルすべてを自動的に空にします。
        db.MustExec(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", tableName))
    }
}

customerテストコード

src/adapter/customer_test.go
package adapter

import (
    "context"
    "testing"

    "github.com/jmoiron/sqlx"

    "github.com/google/go-cmp/cmp"
    ormtest "github.com/sky0621/tips-go/try/ormtest/src"
    "github.com/sky0621/tips-go/try/ormtest/src/repository"
    "github.com/volatiletech/null/v8"
    "github.com/volatiletech/sqlboiler/v4/boil"
)

func TestCustomers(t *testing.T) {
    db := setupDB()
    defer teardownDB(db)

    service := NewCustomerService(db)
    ctx := context.Background()

    tests := []struct {
        name        string
        prepareFunc func()
        want        []*ormtest.Customer
        wantError   bool
    }{
        /*
         * TODO: テストケースは、この順番じゃないと成功しない。本当は"no records"と"some records"はテストケースを分けるべき。
         */
        {
            name:        "no records",
            prepareFunc: func() {},
            want:        []*ormtest.Customer{},
            wantError:   false,
        },
        {
            name: "some records",
            prepareFunc: func() {
                // オプション未指定のデフォルトCustomerを生成(1レコード目なのでIDは 1 になるはず)
                if err := CreateCustomer(ctx, db); err != nil {
                    t.Fatal(err)
                }

                // 全てのパラメータを上書きしてCustomerを生成
                if err := CreateCustomer(ctx, db,
                    withID(2),
                    withFirstName("Satoru"),
                    withLastName("Sato"),
                    withAge(30),
                    withNickname("toru"),
                    withMemo("メモ")); err != nil {
                    t.Fatal(err)
                }
            },
            want: []*ormtest.Customer{
                {
                    ID:       1,
                    FullName: "ダミー姓 ダミー名",
                    Age:      99,
                },
                {
                    ID:       2,
                    FullName: "Sato Satoru",
                    Age:      30,
                    Nickname: "toru",
                    Memo:     "メモ",
                },
            },
            wantError: false,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            tt.prepareFunc()

            got, err := service.Customers(ctx)
            if (err != nil) != tt.wantError {
                t.Errorf("error = %v, wantError = %v", err, tt.wantError)
                return
            }

            opts := []cmp.Option{}
            if diff := cmp.Diff(tt.want, got, opts...); diff != "" {
                t.Errorf("unmatch (-want +got):\n%s", diff)
                return
            }
        })
    }
}

type CustomerOption func(*repository.Customer)

func withID(id int64) CustomerOption {
    return func(c *repository.Customer) {
        c.ID = id
    }
}

func withFirstName(firstName string) CustomerOption {
    return func(c *repository.Customer) {
        c.FirstName = firstName
    }
}

func withLastName(lastName string) CustomerOption {
    return func(c *repository.Customer) {
        c.LastName = lastName
    }
}

func withAge(age int) CustomerOption {
    return func(c *repository.Customer) {
        c.Age = age
    }
}

func withNickname(nickname string) CustomerOption {
    return func(c *repository.Customer) {
        c.Nickname = null.StringFrom(nickname)
    }
}

func withMemo(memo string) CustomerOption {
    return func(c *repository.Customer) {
        c.Memo = null.StringFrom(memo)
    }
}

func CreateCustomer(ctx context.Context, db *sqlx.DB, opts ...CustomerOption) error {
    c := repository.Customer{
        FirstName: "ダミー名",
        LastName:  "ダミー姓",
        Age:       99,
    }
    for _, o := range opts {
        o(&c)
    }
    if err := c.Insert(ctx, db, boil.Infer()); err != nil {
        return err
    }
    return nil
}

お題のところでほぼほぼ説明し終わってるのでソースの記載だけ。
これを実行すると以下のように成功する。
Screenshot at 2020-12-07 01-23-49.png

まとめ

まあ、一長一短あるので、ORM使うならすべてこれっていう感じにはならないと思うけど、1事例として参考程度に。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む