- 投稿日:2020-03-24T20:46:23+09:00
structの処理を差し替えてテストする
現職に就いてからは初めての投稿。
転職に伴い以前は興味半分にしか触っていなかったGoについて本格的に開発で利用することになった。
言語の勉強も兼ねてツールを作って試行錯誤している中で最近やり方を調べながら進めたネタについてのメモで。免責
まだGoを手探りで勉強中で作法とかあまりわかっていないので、その中でこうやるとうまくいったよという一例として閲覧いただければと思います。
(いいやり方かどうかはわかりません、むしろお作法的に良い方法あればコメ投げてください)経緯
ファイルの変更を検知してあれやこれややるツールを作っていて、ファイルイベントの検知に fsnotify を利用している。
import "github.com/fsnotify/fsnotify" func someFunc() error { var w, err = fsnotify.NewWatcher() if err != nil { return err } defer w.Close() if err = w.Add("/path/to/directory"); err != nil { return nil } for { select { ev := <-w.Events: // ファイルイベントを処理 er := <-w.Errors: return er } } }これを使う時にファイルシステム関連で問題があると
NewWatcher()
やw.Add
が error を返すが、ここで エラーを返すケースのテストはどうやるんだろう? という疑問が挙がった。
全体からすると少ない範囲なのでカバレッジの欠けを気にしないでテストしないのも選択肢としてありだし、むしろ筆者のツールの場合はエラー分はログに出したり即時終了する方針なので過剰対応になりそう。
ただ勉強も兼ねて作っているのと調べた内容であまり実装を膨らませずに実現できたので、せっかくだから適用してみた。対応方法
以下のステップで進める。
- エラーを返す処理を直接呼び出さず、非公開の関数型変数に保持して呼び出す
- 関数処理を差し替えてテストする
関数型メンバを定義して呼び出す
呼び出したい関数を一度関数型の変数に詰め込んで、変数を経由して呼び出すようにする。
package notify var newWatcher = fsnotify.NewWatcher func Hoge() { ... w, err := newWatcher() // 関数メンバを実行する }関数を差し替える
プライベート変数は同一パッケージ内からはアクセスできるので、同一パッケージ のテストソースで関数を差し替える処理を追加する。
export_test.gopackage notify // <= ここは実装ソースのパッケージと合わせる func NewWatcherError() func() { t := newWatcher newWatcher = func() (*fsnotify.Watcher, error) { return nil, errors.New("newWatcher error") } return func() { newWatcher = t // テスト後に戻す処理 } }hoge_test.go(利用側)package notify_test func TestHoge(t *testing.T) { // newWatcher を差し替えてテスト終了時に元に戻す defer notify.NewWatcherError()() }インターフェイスを経由してモックする方法(ボツ案)
Goでmockを使ってテストする方法としては、ファイルシステムのテスト や testify/mock などで紹介されているものがある。
これらはいずれも interface を経由して処理の差し替えを行う。
これらを採用しなかった理由としては以下の通り。
ただ今回ボツにしただけで interface 化が必ずしも悪い方法ではなく、むしろポリモーフィズムを使用する場面では必要に応じて使うことになるとは思う。フィールドが利用できない
interface はフィールドを持つことができないので interface の差し替えで処理しようとするとフィールドをそのまま利用することができない。
// 差し替えて使用する用のインターフェイス type nativeWatcher struct { Add(name string) error } // watcher1は Events や Errors のチャンネルを持つ var watcher1 = fsnotify.NewWatcher() // インターフェイスから使用しようとすると Events や Errors は参照できない var watcher2 nativeWatcher = watcher1インターフェイス側でフィールドをラップするレシーバー関数を用意すれば対処できるが、実装をあまり大きくしたくなかった。
structのレシーバー以外は別途インターフェイス化が必要になる
今回エラー検証したかった箇所に
fsnotify.NewWatcher()
が含まれていたが、この処理はレシーバーではないため interface 化できない。
ここをモックライブラリとか使うなら中継インターフェイスが必要になる。// インターフェイスを用意して type watcherGen interface { New() (*fsnotify.Watcher, error) } // 実装を用意して type genImpl struct { } func (gen *genImpl) New() (*fsnotify.Watcher, error) { return fsnotify.NewWatcher() } // 実体を保持する var gen watcherGen = &genImpl{}1処理をモックするための追加としては少々重い。
定義へ移動が使えない
これはEclipse-JavaとかVisual Studioの過去バージョンにあった問題と同様で、開発環境として vscode を使っているが、Goで interface を利用すると処理の実体へジャンプができなくなる。
(多分JavaやC#と違ってインターフェイスの定義を紐付けるのは難しい気がする)func TestXxx(t *testing.T) { w, err := gen.New() // <= 定義へ移動で interface へ飛ばされる }
参考
- 投稿日:2020-03-24T17:43:48+09:00
MarshalJSONで任意の変数をJSON keyに使いたい
struct tagだけでは多分無理なのでjson.Marshaler interfaceを自前で実装してやる。
main.gopackage main import ( "encoding/json" "fmt" ) type RootStruct struct { Values []valStruct } type valStruct struct { JsonKey string `json:"-"` Value string `json:"value"` Error string `json:"error"` } func (s RootStruct) MarshalJSON() ([]byte, error) { data := map[string]interface{}{} for _, val := range s.Values { // json keyはvalStruct側のJsonKeyを参照する data[val.JsonKey] = val } return json.Marshal(data) } func main() { s := RootStruct{} val1 := valStruct{ JsonKey: "key1", Value: "value1", } val2 := valStruct{ JsonKey: "awesome_key", Value: "value2", } val3 := valStruct{ JsonKey: "error_value_key", Value: "", Error: "error message", } s.Values = append(s.Values, val1) s.Values = append(s.Values, val2) s.Values = append(s.Values, val3) data, _ := json.Marshal(s) fmt.Println(string(data)) }実行結果
valStruct
のJsonKeyが利用されてるのがわかる。{ "awesome_key": { "value": "value2", "error": "" }, "error_value_key": { "value": "", "error": "error message" }, "key1": { "value": "value1", "error": "" } }
- 投稿日:2020-03-24T15:28:58+09:00
[Windows] Dockerを使用してMosquittoでMQTTサーバを構築する際に別コンテナにloalhostで接続できるようにする
TL;DR
- Mosquittoを使ってMQTTサーバを立てた際にコンテナ間でlocalhostで接続できないかと調査
- docker-composeでnetwork_modeを「host」にすることでホスト端末と同じIPアドレスにすることで可能とわかった
- GO言語のpaho.mqtt.golangライブラリを使って接続テストを実施
環境
- Windows10
- Docker Desktop 2.2.0.3
- docker-composeはDocker Desktopに同梱
- visual studio code 1.42.1[拡張機能Remote - Containers使用]
- Mosquitto 1.6.9
完成したリポジトリ
https://github.com/MegaBlackLabel/mqtt-docker-sample
ファイル
docker-compose.tmlversion: '3' services: api: build: dockerfile: Dockerfile context: ./containers/api volumes: - ./containers/api/src:/go/api tty: true network_mode: "host" mqtt: build: dockerfile: Dockerfile context: ./containers/mqtt ports: - "1883:1883" volumes: - mosquittodata:/mosquitto/data - mosquittolog:/mosquitto/log tty: true network_mode: "host" volumes: mosquittodata: driver: "local" mosquittolog: driver: "local"
- apiコンテナとmqttコンテナにnetwork_modeで「host」を設定することでホスト端末と同じIPアドレスを使用する
main.gopackage main import ( "crypto/tls" "flag" "fmt" "os" "strconv" "time" MQTT "github.com/eclipse/paho.mqtt.golang" ) // Que Strut. type Que struct { Server string Sendtopic string Resvtopic string Qos int Retained bool Clientid string Username string Password string Client MQTT.Client Callback MQTT.MessageHandler } // Connect func . func (q *Que) Connect() error { connOpts := MQTT.NewClientOptions().AddBroker(q.Server).SetClientID(q.Clientid).SetCleanSession(true) if q.Username != "" { connOpts.SetUsername(q.Username) if q.Password != "" { connOpts.SetPassword(q.Password) } } tlsConfig := &tls.Config{InsecureSkipVerify: true, ClientAuth: tls.NoClientCert} connOpts.SetTLSConfig(tlsConfig) client := MQTT.NewClient(connOpts) if token := client.Connect(); token.Wait() && token.Error() != nil { return token.Error() } q.Client = client if q.Callback != nil { if token := q.Client.Subscribe(q.Resvtopic, byte(q.Qos), q.Callback); token.Wait() && token.Error() != nil { return token.Error() } fmt.Printf("[MQTT] Subscribe to %s\n", q.Sendtopic) } fmt.Printf("[MQTT] Connected to %s\n", q.Server) return nil } // Publish func . func (q *Que) Publish(message string) error { if q.Client != nil { token := q.Client.Publish(q.Sendtopic, byte(q.Qos), q.Retained, message) if token == nil { return token.Error() } fmt.Printf("[MQTT] Sent to %s\n", q.Sendtopic) } return nil } // SetSubscribe - . func (q *Que) SetSubscribe(callback MQTT.MessageHandler) error { if callback != nil { if token := q.Client.Subscribe(q.Resvtopic, byte(q.Qos), callback); token.Wait() && token.Error() != nil { return token.Error() } fmt.Printf("[MQTT] Subscribe to %s\n", q.Sendtopic) } return nil } func onMessageReceived(client MQTT.Client, message MQTT.Message) { fmt.Printf("[MQTT] Received to %s [Received Message: %s]\n", message.Topic(), message.Payload()) } func main() { hostname, _ := os.Hostname() server := flag.String("server", "tcp://localhost:1883", "The full URL of the MQTT server to connect to") sendtopic := flag.String("sendtopic", "MQTT/Client/Update/TEST", "Topic to publish the messages on") resvtopic := flag.String("resvtopic", "MQTT/+/Update/#", "Topic to publish the messages on") qos := flag.Int("qos", 0, "The QoS to send the messages at") retained := flag.Bool("retained", false, "Are the messages sent with the retained flag") clientid := flag.String("clientid", hostname+strconv.Itoa(time.Now().Second()), "A clientid for the connection") username := flag.String("username", "", "A username to authenticate to the MQTT server") password := flag.String("password", "", "Password to match username") flag.Parse() q := &Que{ Server: *server, Sendtopic: *sendtopic, Resvtopic: *resvtopic, Qos: *qos, Retained: *retained, Clientid: *clientid, Username: *username, Password: *password, Callback: onMessageReceived, } err := q.Connect() if err != nil { fmt.Println(err) os.Exit(2) } if err := q.SetSubscribe(onMessageReceived); err != nil { fmt.Println(err) os.Exit(2) } for { time.Sleep(5000 * time.Millisecond) if err := q.Publish("test massage"); err != nil { fmt.Println(err) os.Exit(2) } } }
- MQTTサーバの接続先に「tcp://localhost:1883」を指定しているが、お互いのコンテナがnetwork_modeで「host」を設定しているので接続できる
MQTT接続実行結果
実行結果root@docker-desktop:/go/api# go run main.go go: downloading github.com/eclipse/paho.mqtt.golang v1.2.0 go: downloading golang.org/x/net v0.0.0-20200320220750-118fecf932d8 [MQTT] Subscribe to MQTT/Client/Update/TEST [MQTT] Connected to tcp://localhost:1883 [MQTT] Subscribe to MQTT/Client/Update/TEST [MQTT] Sent to MQTT/Client/Update/TEST [MQTT] Received to MQTT/Client/Update/TEST [Received Message: test massage] [MQTT] Sent to MQTT/Client/Update/TEST [MQTT] Received to MQTT/Client/Update/TEST [Received Message: test massage] [MQTT] Sent to MQTT/Client/Update/TEST [MQTT] Received to MQTT/Client/Update/TEST [Received Message: test massage]まとめ
MQTTサーバをDockerで構築する際にサーバをlocalhostにできないかな、というので調査していてnetwork_mode使えばできることがわかったので記事にしてみました。
- 投稿日:2020-03-24T14:18:26+09:00
3. APIの仕様書を書く
APIってなに?
例えば,ユーザがユーザIDと年月日を入力すると,その日のそのユーザのやわらかさを返す,といったような機能のことです.
なぜ仕様書を書くのか?
どういう値を受け取ってなにを返すのか,自分の頭が整理できます.
またこれから会社に入ってチームで開発するとき,チームメンバーで機能や仕様の共有のためにも仕様書は必須です.逆に仕様書を受け取って開発することもあります.慣れましょう.実装着手するまで結構かかりますが、サーバサイドの開発は設計と検証がすごく重要なのでしっかりと固めながら進めていきましょう!
という名言もいただきました.
仕様書を書いてみよう
今回はSwaggerと呼ばれる,変数名とかをぶちこむだけでいい感じの仕様書を勝手に作ってくれるエディタを使用しました.ブラウザ上で使えます.
こんな感じで左に書き込むと右におしゃれな仕様書ができあがっていきます.すごい.
書き終えたらヘッダーのFile▼から
Save as YAML
で保存しましょう.YAMLはそのまま「ヤムル」と読むそうです.
今回のWebアプリのYAMLはGithubにあります.
- 投稿日:2020-03-24T14:11:17+09:00
2. データベースにどんな属性がはいるのか
どんな値がデータベースに入るのか
Googleのスプレッドシートを用いて,どんな値がデータベースに入るのかを想像して書きました.ここにスプレッドシートを置いておきます.
テーブルは
・ユーザ情報の格納:userテーブル
・やわらかさの格納:yawarakasaテーブル
の2つ用意しました.
Tables_in_yawarakasaapp user yawarakasa userテーブルには下記のような情報が入ると仮定し,
user_id sex Hochiko f Kuno f mama f papa m yawarakasaテーブルには下記の情報が入ると仮定してみました.
user_id year month day yawarakasa Kuno 2020 2 24 30 mama 2020 2 24 31 Kuno 2020 2 26 28 papa 2020 2 27 33 Webアプリ上でユーザが年月日を入力すると,その日のそのユーザのやわらかさを取ってくる感じをイメージしました.
- 投稿日:2020-03-24T14:10:47+09:00
Go標準パッケージのみでWebアプリを作る?
※編集中
おはようございます?
久野です.バックエンドに興味持ったので勉強しつつ何か作ろうと思いWebアプリをつくることにしました.
なにをつくるか
技術選定
- 言語
- Go
- データベース(やわらかさ格納するやつ)
- MySQL
なぜGoか
「軽量・高速・シンプル」と謳われ,流行ってたからです.
Go言語と調べるとこんな感じでいいところがたくさんでてきます▽
2019年大注目のGo言語(Golang)!入門者も知っておきたい特徴や強みはコレ!ソースコード
Githubにあげました.
/api/cmdにあるmain.goを実行すると動きます.$ go run main.goやったこと
- どういう機能を取り入れるか箇条書き
- データベースにどんな属性がはいるのか
- APIの仕様書を書く
- Goで実装していく
1. どういう機能を取り入れるか箇条書き
どんな動きをするWebアプリにするのかを箇条書きでまとめていきます.今回は下記の機能を考えました.
機能
- ユーザ登録
- ユーザID
- 性別
- ユーザ認証
- やわらかさの数値格納
- ユーザごとのやわらかさ表示
- 数値の小さい(やわらかい)順
2. データベースにどんな属性がはいるのか
記事をわけました.こちらです.
3. APIの仕様書を書く
記事をわけました.こちらです.
4. Goで実装していく
記事をわけました.こちらです.
おわりに?
Go言語,サーバー,APIなど全部初めましてなのでちょっと大変でしたが,今までフロントエンドしかやってこなかったのでアプリの裏側を知れて楽しかったです.
フロントエンド,バックエンド,どっちもできるようになってフルスタックマンになりたい.
- 投稿日:2020-03-24T11:38:04+09:00
AtCoderの提出を取得してGitHubの芝を生やすコマンドラインツールを作った
procon-gardener
概要
AtCoderの提出から自動的にコードを取得して、ローカルリポジトリに保存するprocon-gardenerというツールを作りました。
モチベーションとしては競技プログラミングの問題は解いて、もりもりコードは書けるけどGitHubのアクティビティ(芝)がゼロで評価を受けづらい人のために作りました。
我々競プロer勢むけにACするたびに自動pushするツールが求められている・・・
— あやせひろみ (@hiromi_ayase) March 15, 2020ツールではACコードの自動取得とリポジトリへのコミットは行いますが、pushは行いませんので各自で行ってください。
インストール方法
インストールするにはGoが必要です。
go get github.com/togatoga/procon-gardenerサポート環境
- Linux
- macOS
Windowsでは動作確認してませんがサポートしたいのでどなたか動作確認していただけると嬉しいです。
使い方
アーカイブ先のレポジトリをprocon-archiveを用意しました。
1. 設定ファイルの初期化
必要な設定ファイルの作成を行います。
procon-gardener init
を実行してください。% procon-gardener init 2020/03/21 17:18:36 Initialize your config... 2020/03/21 17:18:36 Initialized your config at /home/togatoga/.procon-gardener/config.json2. 設定ファイルの編集
初期化した設定ファイルは以下のとおりです。設定ファイルを直接編集もしくは
procon-gardener edit
で編集することができます。
EDITOR
の環境変数が設定されていれば、EDITOR
に設定されているエディタで開きます。そうでなければOS依存のopen
コマンドで開きます。{ "atcoder": { "repository_path": "", "user_id": "", "user_email": "" } }
repository_path
アーカイブ先のディレクトリを指定してくださいuser_id
アーカイブ対象のユーザーIDを入力してくださいuser_email
repository_path
がGit
リポジトリの場合、git commit
時のメールアドレスに指定されます
user_email
をGitHubの登録メールアドレスに設定しないとGitHubのアクティビティには反映されません。
今回は以下のように設定ファイルを編集しました。{ "atcoder": { "repository_path":"/home/togatoga/src/github.com/togatoga/procon-archive", "user_id": "togatoga", "user_email": "togasakitogatoga+github@gmail.com" } }3. ソースコードのアーカイブ
procon-gardener archive
を実行すれば自動的にファイルがアーカイブされます。 コミットの時間は提出した時間を使っています。
AtCoderへの負荷対策のため1提出につき1.5秒sleepを行っています、AC数が多い人はしばらくお待ちください。
途中で処理の切断やキャンセルしても、既にアーカイブされたコードへのアーカイブ処理は行いません。% procon-gardener archive 2020/03/21 21:19:37 Archiving 1186 code... 2020/03/21 21:19:38 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/abc133/abc133_d/Main.rs Main.rs 2020/03/21 21:19:39 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/abc148/abc148_e/Main.rs Main.rs 2020/03/21 21:19:40 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/abc134/abc134_d/Main.rs Main.rs 2020/03/21 21:19:41 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/abc115/abc115_d/Main.rs Main.rs 2020/03/21 21:19:42 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/agc033/agc033_a/Main.rs Main.rs 2020/03/21 21:19:43 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/abc141/abc141_d/Main.rs Main.rs 2020/03/21 21:19:44 archived the code at /home/togatoga/src/github.com/togatoga/procon-archive/atcoder.jp/ddcc2020-qual/ddcc2020_qual_d/Main.rs Main.rs$ cd /home/togatoga/src/github.com/togatoga/procon-archive/ $ git log commit 412134182e09ab0e165e3499020bcebd80ecfe6d (HEAD -> master) Author: togatoga <togasakitogatoga+github@gmail.com> Date: Sun Mar 15 15:08:28 2020 +0900 [AC] abc141 abc141_d commit d8d36f6cc5ca35ab433b5e6fbabe7ca4e4f7f8bd Author: togatoga <togasakitogatoga+github@gmail.com> Date: Sun Mar 15 16:54:37 2020 +0900 [AC] agc033 agc033_a commit abf4779970804c3fd6fe8bf2d7b2ac02a15e3d34 Author: togatoga <togasakitogatoga+github@gmail.com> Date: Sun Mar 15 18:29:50 2020 +0900 [AC] abc115 abc115_d commit 2615058a482a7f7589d900fd5c84ff8a5ebfc871 Author: togatoga <togasakitogatoga+github@gmail.com> Date: Mon Mar 16 09:42:47 2020 +0900 [AC] abc134 abc134_d commit b84a716762fd4df6df19121b5599b526f2fdba89 Author: togatoga <togasakitogatoga+github@gmail.com> Date: Wed Mar 18 22:12:23 2020 +0900 [AC] abc148 abc148_e commit 7f905746a102190f054430e696da8ab742cffb5c Author: togatoga <togasakitogatoga+github@gmail.com> Date: Fri Mar 20 06:30:19 2020 +0900 [AC] abc133 abc133_dアーカイブ先のディレクトリで
git push
でリモート先を更新しましょう。% cd /home/togatoga/src/github.com/togatoga/procon-archive [master][togatoga] ~/src/github.com/togatoga/procon-archive % git push origin HEAD Enumerating objects: 2346, done. Counting objects: 100% (2346/2346), done. Delta compression using up to 8 threads Compressing objects: 100% (2215/2215), done. Writing objects: 100% (2345/2345), 317.15 KiB | 2.01 MiB/s, done. Total 2345 (delta 799), reused 0 (delta 0) remote: Resolving deltas: 100% (799/799), done. To github.com:togatoga/procon-archive ee2253b..9210fe6 HEAD -> master [master][togatoga] ~/src/github.com/togatoga/procon-archivepush前
push後
AtCoderの問題を解いてGitHubの芝を生やしていきましょう。
バグ報告&要望
要望、バグ報告などはGitHubのissueもしくは@togatoga_まで連絡ください。
- 投稿日:2020-03-24T10:33:19+09:00
【Go】インターフェース(Error)
◎ errorインターフェース
fmtパッケージにあり。
Error()メソッドを利用。
エラーの内容・文字列を返してくれる。使い方
①ストラクト作る(エラーメッセージで必要な要素を設定)
②ストラクトを型にインターフェースのメソッドと同じ名前のメッソドを作る
③インターフェースのメソッドと同じ返り値の型(string)を設定※エラーメッセージのやりとりはポインタを利用する
(ポインタ使わなかった場合、EOFの値が異なり処理がうまくいかない)package main import ( "fmt" ) type NoUser_ErrorMessage struct { Username string } //エラーメッセージ返す用のメソッド作成 func (e *NoUser_ErrorMessage) Error() string { return fmt.Sprintf("User Not found: %v", e.Username) } func myFunc() error { // falseを設定 is_ok := false if is_ok { return nil } return &NoUser_ErrorMessage{Username: "Ken"} } func main() { if err := myFunc(); err != nil { fmt.Println(err) } }結果
User not found: Ken
- 投稿日:2020-03-24T08:09:23+09:00
【Go】インターフェース(タイプアサーション・switch type文)
どんな型が入るかわからない・なんでもOKの場合、
インターフェースを引数にして受け取る[受け取ったときの処理]
・関数内でアサーションしてタイプを確定させる 例:a.(int)
・switch type文で型を判定するのも可能
- 投稿日:2020-03-24T07:50:16+09:00
【Go】インターフェース
[準備]
⓪メソッドらがある
①インターフェースに使いたいメソッドらを指定
②ストラクトを作る
③ストラクトを型にメソッドの処理をつくる (オーバーライド的な?)
[呼び出し]
①インターフェース型の変数を作る
②ストラクトを設定する
③そのストラクトの値でメソッドが実行されるメリット
・インターフェースの型を指定することで利用するメソッド、返り値を指定できる
・異なるストラクトでメソッドが利用できる参考リンク:
https://dev.classmethod.jp/articles/golang-6/
https://qiita.com/rtok/items/46eadbf7b0b7a1b0eb08
https://medium.com/eureka-engineering/golang-embedded-ac43201cf772◎ Stringer interface
・fmtパッケージに入っているインターフェース
文字を返すString()メソッドを持っている・以下のようにインターフェースのメソッドを利用できる。
下記のように表示したいデータのみ表示させることができる。package main import "fmt" type Cat struct { Name string Age int } // String()メソッドを利用すると名前だけ表示し // 他の値は表示しないようにしたりできる func (a Cat) String() string { return fmt.Sprintf("My name is %v.", a.Name) } func main() { cat := Cat{"Kuro", 18} fmt.Println(cat) }結果 (18は表示されない)
My name is Kuro.◎ まとめ
予めあるメソッドはインターフェースを利用して
異なるストラクトでメソッドを定義できる
- 投稿日:2020-03-24T07:50:16+09:00
【Go】インターフェース(ダックタイピング)
[準備]
⓪メソッドらがある
①インターフェースに使いたいメソッドらを指定
②ストラクトを作る
③ストラクトを型にインターフェースに指定したメソッドで処理をつくる (オーバーライド的な)
[呼び出し]
①インターフェース型の変数を作る
②ストラクトを設定する③そのストラクトの値でメソッドが実行される
メリット
・インターフェースの型を指定することで利用するメソッド、返り値を指定できる
・異なるストラクトでメソッドが利用できる参考リンク:
https://dev.classmethod.jp/articles/golang-6/
https://qiita.com/rtok/items/46eadbf7b0b7a1b0eb08◎ Stringer interface
・fmtパッケージに入っているインターフェース
文字を返すString()メソッドを持っている・以下のようにインターフェースのメソッドを利用できる。
下記のように表示したいデータのみ表示させることができる。package main import "fmt" type Cat struct { Name string Age int } // String()メソッドを利用すると名前だけ表示し // 他の値は表示しないようにしたりできる func (a Cat) String() string { return fmt.Sprintf("My name is %v.", a.Name) } func main() { cat := Cat{"Kuro", 18} fmt.Println(cat) }結果 (18は表示されない)
My name is Kuro.◎ まとめ
予めあるメソッドはインターフェースを利用して
異なるストラクトでメソッドを定義できる
- 投稿日:2020-03-24T05:22:55+09:00
Golangで、デザインパターン「Adapter」を学ぶ
GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Adapter」を学ぶ"今回は、Pythonで実装した”Adapter”のサンプルアプリをGolangで実装し直してみました。
■ Adapter(アダプター・パターン)
Adapterパターン(アダプター・パターン)とは、GoF (Gang of Four; 4人のギャングたち) によって定義されたデザインパターンの1つである。Adapterパターンを用いると、既存のクラスに対して修正を加えることなく、インタフェースを変更することができる。Adapterパターンを実現するための手法として"継承を利用した手法"と"委譲を利用した手法"が存在する。
UML class diagram
1. 継承を利用したAdapter
継承を利用したAdapterは、利用したいクラスのサブクラスを作成し、そのサブクラスに対して必要なインタフェースを実装することで実現される。
2. 委譲を利用したAdapter
委譲を利用したAdapterは、利用したいクラスのインスタンスを生成し、そのインスタンスを他クラスから利用することで実現される。
(以上、ウィキペディア(Wikipedia)より引用)■ Golangで、Pythonクラスを実装し直す際の備忘録
Golang では、Python等でお馴染みの、Inheritance(継承)ではなく、Composition(合成)のみが使われます。
具体的、以下の2点を気をつけて、サンプルアプリの再実装に取り組みました。
- golangには、オブジェクトのクラス継承の概念が存在しないので、再利用性(Embedded)と多相性(Interface)の活用で対応する必要がある。
- Pythonの場合だと、動的型付けのおかげて、引数/戻り値の型を意識することなくポリモーフィズムを実践できていたが、Golangの場合では、引数/戻り値の型を柔軟に扱えるようインタフェースを活用する必要がある。
□ 大変、お世話になったQiita記事
■ "Adapter"のサンプルプログラム
実際に、Adapterパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。
- 文字列を、カッコでくくって表示する
- 文字列の前後に、*印をつけて表示する
なお、サンプルプログラムは、再利用性(Embedded)を活用したものと多相性(Interface)を活用したもの、それぞれ用意しました。サンプルプログラム動作の結果は、全く同じになります。
□ 再利用性(Embedded)を利用したサンプルプログラム
$ go run Adapter_1_Embedding/Main.go (Hello) *Hello*□ 多相性(Interface)を利用したサンプルプログラム
$ go run Adapter_2_Interface/Main.go (Hello) *Hello*■ サンプルプログラムの詳細
Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Adapter
- ディレクトリ構成
. ├── Adapter_1_Embedding │ ├── Main.go │ └── adapter │ ├── banner.go │ └── print_banner.go └── Adapter_2_Interface ├── Main.go └── adapter ├── banner.go └── print_banner.go□ 再利用性(Embedded)を利用したサンプルプログラム
(1) Target(対象)の役
Target
役は、インスタンスの振る舞いに関わるインタフェースを定めます。
再利用性(Embedded)を利用したサンプルプログラムでは、この役は存在しません。(2) Client(依頼者)の役
Target
役のメソッドを使って、仕事をする役です。
サンプルプログラムでは、startMain
メソッドが、この役を努めます。Adapter_1_Embedding/Main.gopackage main import ( "./adapter" ) func startMain() { p := adapter.NewPrintBanner("Hello") p.PrintWeak() p.PrintString() } func main() { startMain() }(3) Adaptee(適合される側)の役
Adapter
役のなかで実際に動作するメソッドを、ここで実装します。
サンプルプログラムでは、banner
構造体が、この役を努めます。Adapter_1_Embedding/banner.gopackage adapter import "fmt" type banner struct { str string } func (b *banner) showWithParen() { fmt.Printf("(%s)\n", b.str) } func (b *banner) showWithAster() { fmt.Printf("*%s*\n", b.str) }(4) Adapter(適合する側)の役
Adapter
役は、Target
役のインタフェースを実装しているクラスです。
サンプルプログラムでは、PrintBanner
構造体が、この役を努めます。Adapter_1_Embedding/print_banner.gopackage adapter // PrintBanner is struct type PrintBanner struct { *banner } // NewPrintBanner func for initializing PrintBanner func NewPrintBanner(str string) *PrintBanner { return &PrintBanner{ banner: &banner{str: str}, } } // PrintWeak func for formatting with paren func (p *PrintBanner) PrintWeak() { p.showWithParen() } // PrintString func for formatting with aster func (p *PrintBanner) PrintString() { p.showWithAster() }□ 多相性(Interface)を利用したサンプルプログラム
(1) Target(対象)の役
Target
役は、インスタンスの振る舞いに関わるインタフェースを定めます。
多相性(Interface)を利用したサンプルプログラムでも、この役は存在しません。(2) Client(依頼者)の役
Target
役のメソッドを使って、仕事をする役です。
サンプルプログラムでは、startMain
メソッドが、この役を努めます。Adapter_2_Interface/Main.gopackage main import ( "./adapter" ) func startMain() { p := adapter.NewPrintBanner("Hello") p.PrintWeak() p.PrintString() } func main() { startMain() }(3) Adaptee(適合される側)の役
Adapter
役のなかで実際に動作するメソッドを、ここで実装します。
サンプルプログラムでは、banner
インタフェースに、Compositionを適用して、この役を努めます。Adapter_2_Interface/banner.gopackage adapter import "fmt" type banner interface { showWithParen() showWithAster() } func (p *PrintBanner) showWithParen() { fmt.Printf("(%s)\n", p.str) } func (p *PrintBanner) showWithAster() { fmt.Printf("*%s*\n", p.str) }(4) Adapter(適合する側)の役
Adapter
役は、Target
役のインタフェースを実装しているクラスです。
サンプルプログラムでは、PrintBanner
構造体が、この役を努めます。Adapter_2_Interface/print_banner.gopackage adapter // PrintBanner is struct type PrintBanner struct { str string banner banner } // NewPrintBanner func for initializing PrintBanner func NewPrintBanner(str string) *PrintBanner { printBanner := &PrintBanner{ str: str, } printBanner.banner = printBanner return printBanner } // PrintWeak func for formatting with paren func (p *PrintBanner) PrintWeak() { p.banner.showWithParen() } // PrintString func for formatting with aster func (p *PrintBanner) PrintString() { p.banner.showWithAster() }■ 参考URL
- 投稿日:2020-03-24T00:23:39+09:00
Goのintスライスの値をスペース区切りで出力
int型のスライスのままではstrings.Join()できない
main.gopackage main import ( "fmt" "strings" ) func main() { intSl := []int{1, 2, 3} fmt.Println(strings.Join(intSl, " ")) // => cannot use intSl (type []int) as type []string in argument to strings.Join // strings.Join()の第一引数はstring型のスライスでなければいけない }string型のスライスを作ってからstrings.Join()する
main.gopackage main import ( "fmt" "strconv" "strings" ) func main() { intSl := []int{1, 2, 3} strSl := []string{} for _, v := range intSl { // intSlの値を文字列にしてstrSlに突っ込む strSl = append(strSl, strconv.Itoa(v)) } fmt.Println(strings.Join(strSl, " ")) // => 1 2 3 }ついでに
Pythonならリスト内包表記で1行で書ける
python.pynums = [1, 2, 3] print(" ".join([str(num) for num in nums])) # => 1 2 3