20200324のGoに関する記事は13件です。

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.go
package 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 へ飛ばされる
}

参考

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

MarshalJSONで任意の変数をJSON keyに使いたい

struct tagだけでは多分無理なのでjson.Marshaler interfaceを自前で実装してやる。

main.go
package 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": ""
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.tml
version: '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.go
package 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使えばできることがわかったので記事にしてみました。

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

3. APIの仕様書を書く

APIってなに?

例えば,ユーザがユーザIDと年月日を入力すると,その日のそのユーザのやわらかさを返す,といったような機能のことです.

なぜ仕様書を書くのか?

どういう値を受け取ってなにを返すのか,自分の頭が整理できます.
またこれから会社に入ってチームで開発するとき,チームメンバーで機能や仕様の共有のためにも仕様書は必須です.逆に仕様書を受け取って開発することもあります.慣れましょう.

実装着手するまで結構かかりますが、サーバサイドの開発は設計と検証がすごく重要なのでしっかりと固めながら進めていきましょう!

という名言もいただきました.

仕様書を書いてみよう

今回はSwaggerと呼ばれる,変数名とかをぶちこむだけでいい感じの仕様書を勝手に作ってくれるエディタを使用しました.ブラウザ上で使えます.
スクリーンショット 2020-03-24 14.20.38.png

こんな感じで左に書き込むと右におしゃれな仕様書ができあがっていきます.すごい.

書き終えたらヘッダーのFile▼からSave as YAMLで保存しましょう.YAMLはそのまま「ヤムル」と読むそうです.
今回のWebアプリのYAMLはGithubにあります.

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

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アプリ上でユーザが年月日を入力すると,その日のそのユーザのやわらかさを取ってくる感じをイメージしました.

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

Go標準パッケージのみでWebアプリを作る?

※編集中

おはようございます?

久野です.バックエンドに興味持ったので勉強しつつ何か作ろうと思いWebアプリをつくることにしました.

なにをつくるか

開脚のやわらかさを競うWebアプリをつくります.
yawarakasa

技術選定

  • 言語
    • Go
  • データベース(やわらかさ格納するやつ)
    • MySQL

なぜGoか

軽量・高速・シンプル」と謳われ,流行ってたからです.
Go言語と調べるとこんな感じでいいところがたくさんでてきます▽
2019年大注目のGo言語(Golang)!入門者も知っておきたい特徴や強みはコレ!

あとGopherくんがかわいいからです.
Gopherくん

ソースコード

Githubにあげました.
/api/cmdにあるmain.goを実行すると動きます.

$ go run main.go

やったこと

  1. どういう機能を取り入れるか箇条書き
  2. データベースにどんな属性がはいるのか
  3. APIの仕様書を書く
  4. Goで実装していく

1. どういう機能を取り入れるか箇条書き

どんな動きをするWebアプリにするのかを箇条書きでまとめていきます.今回は下記の機能を考えました.

機能

  • ユーザ登録
    • ユーザID
    • 性別
  • ユーザ認証
  • やわらかさの数値格納
  • ユーザごとのやわらかさ表示
    • 数値の小さい(やわらかい)順

2. データベースにどんな属性がはいるのか

記事をわけました.こちらです.

3. APIの仕様書を書く

記事をわけました.こちらです.

4. Goで実装していく

記事をわけました.こちらです.

おわりに?

Go言語,サーバー,APIなど全部初めましてなのでちょっと大変でしたが,今までフロントエンドしかやってこなかったのでアプリの裏側を知れて楽しかったです.
フロントエンド,バックエンド,どっちもできるようになってフルスタックマンになりたい.

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

AtCoderの提出を取得してGitHubの芝を生やすコマンドラインツールを作った

procon-gardener

概要

AtCoderの提出から自動的にコードを取得して、ローカルリポジトリに保存するprocon-gardenerというツールを作りました。
モチベーションとしては競技プログラミングの問題は解いて、もりもりコードは書けるけどGitHubのアクティビティ(芝)がゼロで評価を受けづらい人のために作りました。

ツールでは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.json

2. 設定ファイルの編集

初期化した設定ファイルは以下のとおりです。設定ファイルを直接編集もしくはprocon-gardener editで編集することができます。

EDITORの環境変数が設定されていれば、EDITORに設定されているエディタで開きます。そうでなければOS依存のopenコマンドで開きます。

{
    "atcoder": {
        "repository_path": "",
        "user_id": "",
        "user_email": ""
    }
}
  • repository_path アーカイブ先のディレクトリを指定してください
  • user_id アーカイブ対象のユーザーIDを入力してください
  • user_email repository_pathGitリポジトリの場合、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-archive

push前

Screenshot from 2020-03-24 11-25-49.png

push後

Screenshot from 2020-03-24 11-26-03.png

AtCoderの問題を解いてGitHubの芝を生やしていきましょう。

バグ報告&要望

要望、バグ報告などはGitHubのissueもしくは@togatoga_まで連絡ください。

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

【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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】インターフェース(タイプアサーション・switch type文)

どんな型が入るかわからない・なんでもOKの場合、
インターフェースを引数にして受け取る

[受け取ったときの処理]

・関数内でアサーションしてタイプを確定させる 例:a.(int)
・switch type文で型を判定するのも可能

・参考リンク:
https://blog.y-yuki.net/entry/2017/05/08/000000

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

【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.

◎ まとめ

予めあるメソッドはインターフェースを利用して
異なるストラクトでメソッドを定義できる

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

【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.

◎ まとめ

予めあるメソッドはインターフェースを利用して
異なるストラクトでメソッドを定義できる

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

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

W3sDesign_Adapter_Design_Pattern_UML.jpg

1. 継承を利用したAdapter

継承を利用したAdapterは、利用したいクラスのサブクラスを作成し、そのサブクラスに対して必要なインタフェースを実装することで実現される。
inheritence.png

2. 委譲を利用したAdapter

委譲を利用したAdapterは、利用したいクラスのインスタンスを生成し、そのインスタンスを他クラスから利用することで実現される。
delegation.png
(以上、ウィキペディア(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.go
package main

import (
    "./adapter"
)

func startMain() {
    p := adapter.NewPrintBanner("Hello")
    p.PrintWeak()
    p.PrintString()
}

func main() {
    startMain()
}

(3) Adaptee(適合される側)の役

Adapter役のなかで実際に動作するメソッドを、ここで実装します。
サンプルプログラムでは、banner構造体が、この役を努めます。

Adapter_1_Embedding/banner.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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

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

Goのintスライスの値をスペース区切りで出力

int型のスライスのままではstrings.Join()できない

main.go
package 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.go
package 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.py
nums = [1, 2, 3]
print(" ".join([str(num) for num in nums]))
# => 1 2 3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む