- 投稿日:2020-12-07T23:56:39+09:00
Goでゲームのロビー機能のようなモノを作る
はじめに
こちらは Go2 Advent Calendar 2020 9日目の記事です。
ゲームのマッチングを行うロビー機能のようなモノをGoで作ってみたので、紹介と簡単な解説をしたいと思います。
ログインしたらロビーでマッチングするまで待機し、相手が見つかったらゲーム画面に接続するというモノです。今回作った内容に、認証機能は含まれていません。作ったもの
connect4という1対1のゲームをベースにしています。
プレイヤー名を入力してログインすると、ロビー(マッチング画面)に移動します。
プレイヤーが2名揃うとプレイ画面が表示されて、ゲームが始まります。仕組み
次の3つのページで構成しています。
- ログインページ:プレイヤー名を入力してログインするためのページ
- ロビーページ:対戦相手のマッチング待つ待機用のページ
- ゲーム画面ログインではクライアント-サーバ間でセッションを作ります。
サーバにプレイヤー名を登録して、クライアントにセッションIDを発行しています。
同時にWebSocketのペアを用意してプレイヤーに割り当てます。ロビーでは、他のプレイヤーが同じWebSocketのペアに割り当てられるのを待機します。
割り当てが終わるとゲーム画面に移動します。ゲーム画面では割り当てられたWebSocketのペアで通信してゲームを進めます。
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 }サーバは複数の
hub
をpool
に持っていて、割り当てを要求されたら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で通知し、クライアントは通知を受け取るとゲーム画面に移動します。今後について
ログイン時に入力したユーザ名が活用できていないのと、使い終わった
hub
をpool
に戻す処理、終了処理が未対応なので、これらは近いうち作ろうと思います。
- 投稿日:2020-12-07T22:33:27+09:00
安く簡単に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ってどんなサービス?
Webアプリや静的サイトのホスティングから、Cron Jobs、自動バックアップ機能付きDBなど、個人のWeb開発で必要なものは一通り揃っています。
SSDディスクも提供されていますので、PostgreSQL意外ももちろん使えます。
無料でonrender.com
のサブドメインも割り当ててくれますし、カスタムドメインも追加できます。
ドキュメントもそれなりに詳しく、サンプルプロジェクト付きで解説があります。
個別に対応していない言語もありますが、Dockerが動くのでなんでもできますね。RenderでGo製サーバーを動かそう
さて、本題のGo言語プロジェクトですが、デプロイに必要な作業はたったの3ステップです。
- サービスタイプを選択
Webサーバーの場合はWeb serversを選ぶと良いです。
Background WorkerはURLを持ちません。
- Gitリポジトリを選択する
GitHubかGitLabからプロジェクトのリポジトリを選択できます。
- サービスの設定
EnvironmentにGoを選択し、Build CommandとStart Commandを指定します。
Regionは現在 Oregon, USA と Frankfurt, Germany が選択できるようですが、日本からだとUSAのほうが速いです。
デフォルトでAuto Deployが有効になっているため、Branchに指定したBranchにpushされると自動的にデプロイされます。
もちろん手動デプロイも可能です。
その他、環境変数やシークレットファイル、追加の永続ディスクなどもこの画面で設定できます。
良いところ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: 安心できる価格設定
今回利用したサービスタイプWeb serversでは最も安いStarterプランで最高 $7/月 です。
しかも利用した分だけ課金されるので無駄に出費する心配もありません。
私の個人開発は趣味や勉強目的なので、サービスを作ったり消したりしていますが毎月300円程度しか請求されていません。おわりに
様々なクラウドコンピューティングサービスを利用した中で、最も入門しやすく、個人開発に十分な機能を備えているので、知らない人には広めていきたいですね。
来年もRenderで個人開発とスキルアップを加速していくぞ!明日はfaidraさんの『なにか』です。
楽しみです!!
- 投稿日:2020-12-07T22:11:08+09:00
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行で書けますが非常に長いので、実際に利用する時は関数を用意してエイリアスを登録して利用しています。
~/.zshrcalias 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
- 投稿日:2020-12-07T22:11:08+09:00
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行で書けますが非常に長いので、実際に利用する時は関数を用意してエイリアスを登録して利用しています。
~/.zshrcalias 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
- 投稿日:2020-12-07T21:37:37+09:00
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
or2
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 stepsExample 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考え方
こちら冷静に考えてみるとn番目のフィボナッチ数を求めるのと同じ意味で出題されているとわかります。
今回はループ処理で解いてみました。再帰的に説くことも可能です。
- 解答コード
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 }
- 投稿日:2020-12-07T18:58:59+09:00
【Go言語で学ぶ】抽象化
皆さんこんにちは!
シュークリーム大好きエンジニアのくろちゃんです。こちらの記事は、CA21 Advent Calendar 2020の7日目の記事です!
最近QiitaやTwitterでのアウトプットが全くできていなかったので、アドベントカレンダーを良いきっかけにできたら嬉しいなと思っています背景
実は、3週間ほど前から23卒サーバサイドエンジニアの子を鍛え上げるというプロジェクトでメンターをやらせていただいています。
当面の目標をGo言語を用いたAPIサーバ開発ができるようになることを目標に置き、Go言語の基本構文から勉強してもらっています。
その中で、抽象化について学んでもらおうと思った際に上手い教え方ができず、結局何が便利だから抽象化するんだっけ?というところまで伝えられませんでした。
この体験を元に、自分自身抽象化についてもう一度学び直しました。
この記事を読んだ23卒の子が抽象化について少しでも理解を深めてくれたら良いなと思って記事にまとめます。そもそも抽象化って?
プログラミング以外の場面でも、「抽象化して考えてみよう!」などと言ったりしますが、そもそも抽象化とは具体的に何をしたら抽象化できた事になるのでしょうか?
まずは抽象化の意味について調べてみました。
▼抽象化の意味(Wikipediaから引用)
思考における手法のひとつで、対象から注目すべき要素を重点的に抜き出して他は捨て去る方法である
何だかわかりそうで分からない・・・・。
個人的な解釈でいうと、要は「グルーピングする事」なのではないかと考えています。例えば、自動車を抽象化する時のことを考えてみましょう。
まずは、具体的な自動車を2台頭に浮かべます。スポーツカーと軽自動車を思い浮かべてみてください。この2台比較したときに共通の要素を抜き出してみてください。
- アクセルを踏むと前進する
- ブレーキを踏むと止まる
- ライトが付いている
- 乗るには免許証が必要
などでしょうか?今回はスポーツカーと軽自動車を比べましたが、ここにトラックや電気自動車なども加えることで、共通する部分が抜き出しやすくなるのではないでしょうか。
上記で挙げたような要素を満たすモノのことを私たちは一般的に車や自動車と呼んでいますね?
単体で見れば、スポーツカーと軽自動車では出せるスピードが違ったり、燃費も違ったりします。ですが、共通点として挙げた「アクセルを踏むと前進する」といった特徴はスポーツカーや軽自動車特有の特徴ではありません。
自動車というグループに所属するモノ全てが持ち合わせている特徴です。僕はこのように、具体的な1つ1つのモノ同士を比べて共通点を探し、グルーピングする事が抽象化するという事だと考えています。
プログラミングにおける抽象化
前章で抽象化そのものの考え方について見てきました。
プログラミングにおける抽象化も、先ほど説明した考え方とほぼほぼ一緒で、基本的には共通項を切り出してグルーピングするというモノになります。ですが、プログラミングにおける抽象化はどちらかというと、実装の詳細を隠蔽化して利用できるようにするという文脈で使われる事が多いように感じており、そこを説明しなかったからこそ、今回抽象化の必要性を感じてもらえなかったのかなーと考えています。
ここからは、Go言語のサンプルコードをお見せしながら、
- 共通項を切り出してグルーピングする抽象化
- 実装の詳細を隠蔽する抽象化
の2つについて詳しく説明します!
共通項を切り出してグルーピングする抽象化
こちらの抽象化は、僕たちが普段日常生活などでも行っている抽象化に近い考え方であるため、比較的簡単にイメージする事が可能です。
しかし、抽象化をするメリットがよく分からないという落とし穴にハマりがちなのかなとも感じています。僕がトレーニーから「抽象化については何となくわかったけれど、何が便利なのか分かりません!」と言われた例を示します。よくある、動物を題材に取り上げたサンプルコードとなっています。
animal/interface.gopackage animal type Animal interface { Bark() Eat() Die() }まずは、動物というグルーピングをした時に、
- 鳴く
- 食べる
- 死ぬ
という3つの動作は共通化して切り出せそうなので、interfaceとして切り出します。
次に、animal
インターフェースを満たすDog
とCat
の詳細実装をします。dog/dog.gopackage 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.gopackage 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.logdog/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.gopackage 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
というインターフェースにまとめました。
現実世界での抽象化と同じような思考プロセスを辿るため、比較的すんなり理解できたのではないでしょうか?しかし、この例では抽象化という考え方は学べても、プログラミングする上で何が嬉しいのか正直よく分かりません。(よね?)
実装の詳細を隠蔽する抽象化
個人的にプログラミングする中で得られる抽象化のメリットと感じているのは、実装の詳細を隠蔽して、関数を使わせることができることだと思っています。
実装の詳細とは、関数内部で行っている具体的な処理のことを指します。
例えば、ユーザを作成する
という機能について考えたときの実装の詳細は下記のような一連の処理を指します。
- ユーザから受け取った値をバリデーションする
- UUIDを生成する
- ユーザモデルにマッピングする
- MySQLのUserテーブルに対して
INSERT
を実行するこの時、この関数を使う側からすると、「中身の処理がどうなっていようと、そんなことはどうでもいいからユーザを作成してください!」と思うのではないでしょうか?
実装の詳細を隠蔽することで得られる実装上のメリット
開発者にとっても、実装詳細の隠蔽化をすることで得られるメリットがあります。
それは、コードの柔軟性を上げられるというメリットです!まずは、設計者になったつもりで次のようなことを考えてみましょう。
ユーザに関するいくつかの仕様が固まったとします。今回は仮に、
- ユーザ作成
- ユーザ情報取得
- ユーザ情報更新
- ユーザと講座情報紐付け
という4つの機能を実装することになりました。
この時、設計者であるあなたが仕様通りの機能を全て実装するのは非常にナンセンスです。できればチームメンバーに中身の実装はお願いしたいところですね!
そんな時役立つのが抽象化です!設計者であるあなたは、上記4つの機能の入力と出力だけ定めたインターフェースを作成するだけで、あとの中身の実装は他の人に任せることができます。
任された人も、インターフェースを満たすようにさえすれば良いので、機能ごとの詳細な実装に集中することができます。
また、インターフェースを満たすものであれば同じインターフェースから複数個インスタンスが生成できるため、
MySQLへの操作に使うインスタンス
・Redisへの操作を行うインスタンス
などのように、中で使用している技術に応じてインスタンスを分けていくことが可能です。そうしておけば、使う側も用途に合わせて適切なインスタンスを生成し、利用することで「あくまで
ユーザを作成する
などのような粒度」で扱うことができます。上記のような抽象化のメリットを少しでも感じていただけるように、サンプルコードを作成しました。
GitHubにてコードを公開していますので、そちらも合わせてご覧ください。まずは
UserInterface
という名前のインターフェースを定義します。type UserInterface interface { PrintMyData() UpdateBaseData(entity.UserData) }これを見ただけで、ユーザは
自分のデータを出力する機能
と基本データを更新するための機能
が存在することが分かりますね。このインターフェースの詳細実装を見ていきましょう。
今回実装されているのは、adult
とchild
の2つです。adult.gopackage 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.gopackage 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.gotype Adult struct { entity.UserData Phone string `json:"phone"` Married bool `json:"married"` }child.gotype Child struct { entity.UserData Gender GenderType `json:"gender"` }インターフェースを基にしてそれぞれ異なった構造体をもち、それぞれの要件に合わせた実装がされていると見ることができます。
これらの生成されたインスタンスを使う側は、特に実装の中身を意識することなく、生成したインスタンスで公開されている関数を利用することができます。
main.gopackage 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をお楽しみください!!みなさんにとって今年の締め括りが素晴らしいものになることを心から祈っています。ではっ
- 投稿日:2020-12-07T17:11:09+09:00
GO言語でカプセル化
以前に書いた記事 GO言語でクラスっぽいことをする の中で触れられなかった「カプセル化をGo言語で実現する方法」について書いていこうと思います。
ポリモフィズムに関しては今回も省略します。(いつか書きたい)やりたいこと
- コンストラクタでPrivate変数の初期値を設定する
- Publicメソッドを外部から参照して実行する
- Publicメソッド内でPrivate変数を利用する
- Publicの定数を参照して利用する
- Private変数の値を参照しようとするとエラーになることを確認する
全網羅じゃないけどよく使いそうな処理
実装例
main.gopackage 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.gopackage 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.gopackage 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言語でカプセル化するには
- パッケージを分ける
- 外部から参照されたくない要素は「先頭小文字」で定義する
- 投稿日:2020-12-07T16:15:01+09:00
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) をご覧ください。
- 投稿日:2020-12-07T13:52:56+09:00
Hugoで自作ブログサイトを作ってみる #1
※これは自作ブログに投稿したものと同じ記事です。
Hugoで自作ブログサイトを作ってみるはじめに
最近、Twitter の有名なすごい人たちがみんな Hugo を使って自作ブログを作っていて、ほげーーやってみたいーー、ってなったので試してみた。
また、Go 製のツールということで、勉強したかった Go を使えるかも!という思いもあった。(結果、Go は 1 ミリも使わずに終わった)
さんぽしさん
Hugoでさくっと自作ブログを作った – さんぽしの散歩記
コミさん
Hello My New BlogHugo とは?
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 は本家を参照してください。
入ったら、次のコマンドで雛形が生成される
$ hugo new site blog && cd blog
$ hugo new site {好きな名前}
で作られるそうすると次のようなものたちが生成される
. ├── archetypes │ └── default.md ├── config.toml ├── content ├── data ├── layouts ├── static └── themes
現時点だと、全部空のディレクトリがあるだけ。
テンプレートテーマを適用していく
先ほどのサイトからテーマを選ぶ。
今回は、Timer Hugo
というものを選んだ。レスポンシブに対応してるし、結構いろいろカスタムできそうだったから。さっき生成した 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
しばらくすると公開される。やったね!
まとめ
これでサクッと雛形が作れました。テーマもお洒落なのがたくさんあるので見てるだけで楽しい!
次回はブログをカスタマイズしていくところを書きたい。(これが地獄の始まりだった…)
参考
参考させていただいた素晴らしい記事たちです
- 投稿日:2020-12-07T13:52:56+09:00
HugoとGithub Pagesで自作ブログサイトを作ってみる #1
※これは自作ブログに投稿したものと同じ記事です。
Hugoで自作ブログサイトを作ってみるはじめに
最近、Twitter の有名なすごい人たちがみんな Hugo を使って自作ブログを作っていて、ほげーーやってみたいーー、ってなったので試してみた。
また、Go 製のツールということで、勉強したかった Go を使えるかも!という思いもあった。(結果、Go は 1 ミリも使わずに終わった)
さんぽしさん
Hugoでさくっと自作ブログを作った – さんぽしの散歩記
コミさん
Hello My New BlogHugo とは?
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 は本家を参照してください。
入ったら、次のコマンドで雛形が生成される
$ hugo new site blog && cd blog
$ hugo new site {好きな名前}
で作られるそうすると次のようなものたちが生成される
. ├── archetypes │ └── default.md ├── config.toml ├── content ├── data ├── layouts ├── static └── themes
現時点だと、全部空のディレクトリがあるだけ。
テンプレートテーマを適用していく
先ほどのサイトからテーマを選ぶ。
今回は、Timer Hugo
というものを選んだ。レスポンシブに対応してるし、結構いろいろカスタムできそうだったから。さっき生成した 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
しばらくすると公開される。やったね!
まとめ
これでサクッと雛形が作れました。テーマもお洒落なのがたくさんあるので見てるだけで楽しい!
次回はブログをカスタマイズしていくところを書きたい。(これが地獄の始まりだった…)
参考
参考させていただいた素晴らしい記事たちです
- 投稿日:2020-12-07T11:16:47+09:00
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 if
、else
ブロック内でも使える- 同じ名前の変数を宣言した場合、上書きされる(=スコープがネストされる?)
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 = 3Switch 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 言語は言語の仕様変更・追加が少ないので、数年前の記事が当てになるのが良い
- 投稿日:2020-12-07T10:37:12+09:00
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のケースでも試してみたいです.
- 投稿日:2020-12-07T08:59:53+09:00
Dockerを使ってPostgreSQL+Goの開発環境を作ってみた
はじめに
GoからPostgreSQLに接続する部分がよくわかってなかったので、自分でDocker環境作って試してみた。
参考
ここを大いに参考にさせてもらった。
Go PostgreSQLにつないでみる - Qiita
Docker で作る postgres 環境 | CrudzooDocker
ポイントはnetworks部分を同じにしてるところ。
ネットワークの準備
# docker network create postgres-test-network # docker network ls NETWORK ID NAME DRIVER SCOPE xxxxxxxxxxxx postgres-test-network bridge localPostgreSQL
こんな感じ。
POSTGRES_HOST_AUTH_METHOD: true
は推奨してないって言われたけど、ローカルのお試し環境だから無視した。DockerfileFROM postgres:11-alpine ENV LANG ja_JP.utf8docker-compose.ymlversion: '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 -dGo
DockerfileFROM golang:latest RUN mkdir /go/src/work WORKDIR /go/src/work ADD . /go/src/workdocker-compose.ymlversion: '3' services: app: build: . tty: true volumes: - .:/go/src/work networks: default: external: name: postgres-test-networkこれで立ち上げる
docker-compose up -dアクセス部分
Goの方のコンテナの中では、これを実行する。
ここをほぼほぼコピペさせてもらった。
Go PostgreSQLにつないでみる - Qiitamain.gopackage 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.goPosticoで確認
一応、DBの中身もGUIツールで確認した。良い感じ。
Postico – a modern PostgreSQL client for the Mac
おわりに
ローカルでに動く環境作れたので、いろいろ試してみたい。
- 投稿日:2020-12-07T05:47:09+09:00
【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の間にアクセスが集中すると、それらは全てデータベースに同じデータを取得しに行ってしまい、無駄が多いです。そこで、誰かがデータを取得しに行ってる間、同じデータが必要な後続リクエストはそれを待機する様にしたいと思います。
作戦
先発リクエストデータ取得中の後続リクエストの待機にChannelを利用します。
Channelはキャッシュのエントリ毎に保持します。
- 先発リクエストはまず空のキャッシュエントリを作成(予約)しChannelを保存する
- 先発リクエストがデータ取得開始
- 後続リクエストは1で作成されたChannelをread。先発リクエストのデータ取得を待機
- 先発リクエストはデータを取得したらエントリに格納し、Channelをclose
- 待機していた後続リクエストが動き出しデータを参照
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を参照ください。
本記事内で使用しているGopher画像はRenée Frenchさんのデザインを加工したものです ↩
- 投稿日:2020-12-07T01:24:51+09:00
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(~~)
関数を実行した時に、Customer
のFirstName
がデフォルトの「"ダミー名"」から、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.tomlDB
docker-composeで立ち上げる。
普通にアプリ起動した時につなぐDBとテストコードからアクセスするDBは分けてる。
(じゃないと、アプリ起動して動作確認してる時とテストコード流してる時とでデータが混ざる。そしてテスト結果が変わりうる。)db/docker-compose.ymlversion: '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.ymllocal: 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.tomloutput = "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.gopackage 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形式ですらない。アクセスしても以下のようにそっけなく構造体の中身がブラウザに表示されるだけ。
カスタマーサービスロジック
インタフェースは下記。
src/customer.gopackage 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.gopackage 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.gopackage 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.gopackage 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 }お題のところでほぼほぼ説明し終わってるのでソースの記載だけ。
これを実行すると以下のように成功する。
まとめ
まあ、一長一短あるので、ORM使うならすべてこれっていう感じにはならないと思うけど、1事例として参考程度に。