20200326のGoに関する記事は7件です。

Azure Functions で Go 言語を使おう(Go 1.14.2 待ち)

Azure Functions custom handler というものを使うと、Web API を作れる言語なら、なんでも Azure Functions の上で動かせるようになります!!まだプレビューですけど、楽しみな機能です!!

端的に言うと、Azure Functions がサポートしていない言語での開発や、Azure Functions がサポートしている言語でも特定の Web API を開発するフレームワークを使って開発するといったことが出来ます。サポートしていない言語での開発だと Go 言語で開発出来たり、後者の例だと Node.js で express を使って開発したり C# で ASP.NET Core を使って開発したりといったことが出来ます。

ドキュメント:Azure Functions custom handlers (preview)

仕組み

host.json に裏で動く HTTP サーバーを起動するコマンドを設定しておくと、裏で HTTP サーバーを起動して Azure Functions の関数が呼ばれたときに /関数名 という名前のパスに HTTP リクエスト投げてくれる。レスポンスの JSON を特定の形式に従っておくと、Functions のバインドによしなに渡してくれるといった感じです。

シンプルな HTTP トリガーで起動して HTTP レスポンスを返すだけなら普通に作れば OK です。そうじゃない Queue トリガーなどの他のトリガーや、バインドを使う場合は、ドキュメントからの引用ですが、こんな感じの function.json の場合は

{
  "bindings": [
    {
      "name": "myQueueItem",
      "type": "queueTrigger",
      "direction": "in",
      "queueName": "messages-incoming",
      "connection": "AzureWebJobsStorage"
    },
    {
      "name": "$return",
      "type": "queue",
      "direction": "out",
      "queueName": "messages-outgoing",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

こんな感じの JSON がリクエスト Body に入ってきます

{
    "Data": {
        "myQueueItem": "{ message: \"Message sent\" }"
    },
    "Metadata": {
        "DequeueCount": 1,
        "ExpirationTime": "2019-10-16T17:58:31+00:00",
        "Id": "800ae4b3-bdd2-4c08-badd-f08e5a34b865",
        "InsertionTime": "2019-10-09T17:58:31+00:00",
        "NextVisibleTime": "2019-10-09T18:08:32+00:00",
        "PopReceipt": "AgAAAAMAAAAAAAAAAgtnj8x+1QE=",
        "sys": {
            "MethodName": "QueueTrigger",
            "UtcNow": "2019-10-09T17:58:32.2205399Z",
            "RandGuid": "24ad4c06-24ad-4e5b-8294-3da9714877e9"
        }
    }
}

レスポンスの JSON は

{
  "Outputs": {
    "出力バインドの名前": 出力バインドに渡す値
  },
  "Logs": ["log message1", "log message2"],
  "returnValue": $return に渡す値
}

のような感じになります。

試してみよう

シンプルな HTTP トリガー対応なら純粋に JSON を受け取って JSON を返すようなものがあればいいので、やってみましょう。とりあえず Go 言語で!

package main

import (
    "encoding/json"
    "net/http"
    "os"
    "time"
    "fmt"
    "log"
)

type user struct {
    Name string
    Age  int
}

func handleRequests() {
    http.HandleFunc("/Hello", func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        fmt.Println(t.Month())
        fmt.Println(t.Day())
        fmt.Println(t.Year())
        ua := r.Header.Get("User-Agent")
        fmt.Printf("user agent is: %s \n", ua)
        invocationid := r.Header.Get("X-Azure-Functions-InvocationId")
        fmt.Printf("invocationid is: %s \n", invocationid)      
        json.NewEncoder(w).Encode(user{
            Name: "Kazuki from /hello",
            Age:  39,
        })
    })
    httpInvokerPort, exists := os.LookupEnv("FUNCTIONS_HTTPWORKER_PORT")
    if exists {
        fmt.Println("FUNCTIONS_HTTPWORKER_PORT: " + httpInvokerPort)
    }
    log.Println("Go server Listening...on httpInvokerPort:", httpInvokerPort)
    log.Fatal(http.ListenAndServe(":"+httpInvokerPort, nil))}

func main() {
    handleRequests()
}

このコードを go build main.go で main.exe にして、host.json を以下のようにします。

{
    "version": "2.0",
    "httpWorker": {
        "description": {
            "defaultExecutablePath": "main.exe"
        }
    }
}

そして、/Hello でリクエストを受け取るので Hello フォルダーを作って、その中に function.json を作ってシンプルな Http トリガーの関数の定義をします。

{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ]
}

因みに、ファイルは以下のような位置関係で配置しています。

image.png

そして、ローカルで実行してみます。func host start で実行すると、以下のように Hello 関数がちゃんと Http で受け付けるようになっているのがわかります。

image.png

実はこの時 main.exe が裏で起動しています。タスクマネージャーで見てみるとちゃんといますね。

image.png

関数の URL を叩いてみると Go 言語で返した JSON がちゃんと帰ってきました!

image.png

いいね。

Azure 上で動かしてみよう

デプロイ自体は、とりあえず試すだけなら func azure functionapp publish デプロイ先の関数アプリ名 --force というコマンドでデプロイできます。(main.go とかも、もれなく一緒にデプロイされてしまいますが…!!)
ただ、残念ながら Azure App Service (Azure Functions とかが動くサービス)上で Go 言語の 1.14.1 でビルドしたバイナリを動かそうとすると以下のようなエラーが出ます。

image.png

調べてみたら Go 1.14.2 で修正されるみたいなので、もうちょっとお預けですね…!

PowerRegisterSuspendResumeNotification error on Azure App Services with go 1.13.7 [1.14 backport]

Issue の最終更新が 16 時間前とか、ホカホカの Issue でした。

image.png

ということで Go 1.14.2 がでたら、また試してみようと思います!

まとめ

Azure Functions 上で任意の言語で開発したプログラム動くようになるのは個人的にはアツイ。関数の実行時間が制限時間をオーバーしたときにどうなるの?とか気になる点はありますが、正式リリースのころにはドキュメントなども整備されると思うので、その時にまた調べてみようと思います。

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

【Go】Lambda + RDS 接続にRDS Proxyを使ってみた

はじめに

現在、API Gateway + Lambda + RDSを使ってWebアプリケーションを作っています!2019年末に行われたre:Invent 2019で発表された、RDS Proxy(現在はプレビュー版です。)を試してみたので備忘録です。

RDS Proxyってなに?

簡単に言うと、データベースへのコネクションプールを確立、管理することで、アプリケーションからのデータベース接続数を少なく抑えることができるサービスです。

Lambda関数は、呼び出すごとに新しいコネクションを作成する必要があります。しかし、LambdaからRDSへの同時接続数には上限があり、これまではコネクション数が上限に達しないようにする必要がありました。これを解決してくれるのがこのRDS Proxyです。RDS Proxyを利用することで、既存のコネクションを再利用することができ、コネクション数を抑えることができます。

つまり、Lambda + RDSの構成が避けられていた原因の1つの同時接続数問題が解決できるのです!

構成図

このような構成で作成しました!本記事では、Lambda、RDS Proxy、RDS、踏み台のEC2にフォーカスしています。
RDS Proxy構成図.png

手順

下記の流れで進めていきます。

  1. VPC、サブネットの作成
  2. セキュリティグループの作成
  3. RDSの構築
  4. 踏み台EC2の構築
  5. テーブル作成
  6. DBユーザの作成
  7. RDS Proxyの構築
  8. Lambda関数の作成

やってみる

1. VPC、サブネットの作成

事前準備として、VPCを作成し、作成したVPCの中にプライベートサブネット、パブリックサブネットを作成します。特別な設定は不要なので作成方法は省略します。

2. セキュリティグループの作成

事前準備として、各リソースのセキュリティグループの作成を行います。

Lambda

セキュリティグループ name : sg-lambda
インバウンド 

タイプ ポート ソース

特にどこからも許可していなくてもAPI Gatewayからは叩くことができます!

EC2

セキュリティグループ name : sg-ec2-bastion
インバウンド

タイプ ポート ソース
SSH 22 許可したいIPアドレス

RDS Proxy

セキュリティグループ name : sg-rdsproxy
インバウンド

タイプ ポート ソース
MySQL/Aurora 3306 sg-lambda

RDS

セキュリティグループ name : sg-rds
インバウンド

タイプ ポート ソース
MySQL/Aurora 3306 sg-ec2-bastion
MySQL/Aurora 3306 sg-rdsproxy

3. RDSの構築

プライベートサブネットにRDSを立てます。
今回使用したMySQLのバージョン : MySQL 5.7.22
セキュリティグループは2で作成したsg-rdsを選択してください。

その他、特に特別な設定は不要なので省略します。

4. 踏み台EC2の構築

RDSに接続して、ユーザやテーブルを作成するための踏み台EC2をたてます。
今回使用したOS : Amazon Linux 2
セキュリティグループは2で作成したsg-ec2-bastionを選択してください。

こちらも、特に特別な設定は不要なので省略します。
ただ、IPv4パブリックIPを使ってsshで接続するため、自動割り当てパブリックIPは有効に設定する。(これをせずに、プライベートIPで接続を試みましたが、できませんでした。)

※上記の方法(自動割り当てパブリックIP)では、EC2を再起動するごとにパブリックIPアドレスが変更になるのでお気をつけてください。

スクリーンショット 2020-03-11 19.38.45.png

5. テーブル作成

RDS内にテーブルを作成します。
テーブル内のデータはcsvファイルから取り込むようにしたかったので、今回はcsvファイルを準備しましたが、ただデータをRDSに保存するだけです。

csvファイルのアップロード

ローカルから踏み台のEC2にcsvファイルを移動させました。

$ scp -i [キーペア名].pem [ファイル名].csv ec2-user@[パブリックIP]:/home/ec2-user/(EC2内の保存したいディレクトリを指定)

EC2にssh接続

$ ssh -i [キーペア名].pem ec2-user@[IPv4パブリックIP]

MySQLのインストール

$ sudo yum update
$ sudo yum install mysql

RDSへ接続

$ mysql --local-infile=1 -h [RDSエンドポイント] -u [マスタユーザ名] -p

テーブル内のデータはcsvファイルから読み込むために、--local-infile=1のオプションをつけました。

テーブルの作成

まず、テーブルの枠を作成します。

> CREATE TABLE [テーブル名] ([カラムの指定]);

# 例
> CREATE TABLE m1_champion (id INT(2) AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(30) NOT NULL, champion YEAR NOT NULL, formed YEAR NOT NULL, note VARCHAR(30));

次に、下記のようなcsvファイルをインポートします。

スクリーンショット 2020-03-12 18.23.55.png

> LOAD DATA LOCAL INFILE "[ファイルパス]/[ファイル名].csv" INTO TABLE [テーブル名] FIELDS TERMINATED BY ',' LINES TERMINATED BY '\r\n';

これでテーブルが完成しました。

id name champion formed note
1 ミルクボーイ 2019 2007 コーンフレーク
2 霜降り明星 2018 2013 null
3 とろサーモン 2017 2002 null

6. DBユーザの作成

上記の手順通り進めば、現在DBにログインしているので、このままLambdaから接続したときに使うユーザを作成します。

> CREATE USER '[ユーザ名]'@'%' IDENTIFIED BY '[パスワード]';
> GRANT SELECT, INSERT, UPDATE, DELETE ON [対象のDB].[対象のテーブル] TO '[ユーザー名]'@'%';

上記はSELECT, INSERT, UPDATE, DELETEの権限を許可しています。また、ホスト名には%=ワイルドカードを使用し、どこからのアクセスも受け入れるように設定しています。ちなみに、全てのDBとテーブルが対象の場合は*.*とします。

7. RDS Proxyの構築

いよいよRDS Proxyの構築に入ります。

Secrets Manager シークレットの作成

先ほど作成したDBのユーザ名とパスワードを入力し、作成します。
下記URLのAWSの公式ブログに沿って作成してください。
参考:AWS LambdaでAmazon RDS Proxyを使用する

IAMロールの作成

ユースケースはRDS - Add Role to Databaseを選択します。
スクリーンショット 2020-03-11 19.32.53.png

そして、必要なポリシーをアタッチします。

Secrets Managerへのアクセス権限のポリシー
{
    "Version": "2012-10-17",
    "Statement": [

        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetResourcePolicy",
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:ListSecretVersionIds"
            ],
            "Resource": "[シークレットARN]"
        }
    ]
}

拡張ログを取得したい場合はCloud Watch Logsへのアクセス権限も必要です。ただ、以下のログに関しては、Cloud Watch Logsへのアクセス権限がなくてもロググループに出力されます。

  • RDS Proxyの起動終了
  • DBへの接続開始終了
  • 警告

RDS Proxyの作成

今回はRDS Proxyのコンソールから作成します。

ちなみに、Lambda関数のコンソール上からも作成でき、IAM認証で接続する場合はLambdaと紐付ける手間を省くことができます。ただ、今回のようにDBのユーザ名とパスワードを用いて接続する場合は紐付けは必要ないようなので、RDS Proxyのコンソールから作成しました。

スクリーンショット 2020-03-16 12.27.47.png

プロキシ識別子を入力します。

スクリーンショット 2020-03-16 12.27.56.png

先ほど作成したRDSを選択します。

スクリーンショット 2020-03-16 12.25.36.png

先ほど作成したSecrets ManagerシークレットとIAMロールを選択します。
サブネットはRDSと同じプライベートサブネットを選択します。
セキュリティグループは2で作成したsg-rdsproxyを選択してください。

スクリーンショット 2020-03-16 12.25.49.png

Cloud Watch Logsでデバッグログを取得したい場合は拡張されたログ記録を有効にするにチェックを入れてください。(Cloud Watch Logsへのアクセス権限も必要です。)

8. Lambda関数の作成

IAMロール

LambdaはVPC内にあるのでENI生成用のポリシーを作成してアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface",
                "ec2:CreateNetworkInterface"
            ],
            "Resource": "*"
        }
    ]
}

ログを取得したい場合はCloud Watch Logsへのアクセス権限も必要です。

VPC

Lambda関数の編集画面で設定できます。

スクリーンショット 2020-03-16 12.26.00.png

カスタムVPCを選択し、RDSとRDS Proxyと同様のVPCとパブリックサブネットを選択します。
セキュリティグループは2で作成したsg-lambdaを選択してください。

ソースコード

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    _ "github.com/go-sql-driver/mysql"
    "os"
)

type Response struct {
    ID       int            `json:"id"`
    Name     string         `json:"name"`
    Champion string         `json:"champion"`
    Formed   string         `json:"formed"`
    Note     sql.NullString `json:"note"`
}

// os.Getenv()でLambdaの環境変数を取得
var dbUser = os.Getenv("dbUser")         // DBに作成したユーザ名
var dbPass = os.Getenv("dbPass")         // パスワード
var dbEndpoint = os.Getenv("dbEndpoint") // RDS Proxyのプロキシエンドポイント
var dbName = os.Getenv("dbName")         // テーブルを作ったDB名

func RDSConnect() (*sql.DB, error) {
    connectStr := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=%s",
        dbUser,
        dbPass,
        dbEndpoint,
        "3306",
        dbName,
        "utf8",
    )
    db, err := sql.Open("mysql", connectStr)
    if err != nil {
        panic(err.Error())
    }
    return db, nil
}

func RDSProcessing(db *sql.DB) (interface{}, error) {

    var id int
    var name string
    var champion string
    var formed string
    var note sql.NullString

    responses := []Response{}
    responseMap := Response{}

    getData, err := db.Query("SELECT * FROM m1_champion")
    defer getData.Close()
    if err != nil {
        return nil, err
    }

    for getData.Next() {
        if err := getData.Scan(&id, &name, &champion, &formed, &note); err != nil {
            return nil, err
        }
        fmt.Println(id, name, champion, formed, note)
        responseMap.ID = id
        responseMap.Name = name
        responseMap.Champion = champion
        responseMap.Formed = formed
        responseMap.Note = note
        responses = append(responses, responseMap)
    }

    params, _ := json.Marshal(responses)
    fmt.Println(string(params))

    defer db.Close()
    return string(params), nil
}

func run() (interface{}, error) {
    fmt.Println("RDS接続 start!")
    db, err := RDSConnect()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println("RDS接続 end!")
    fmt.Println("RDS処理 start!")
    response, err := RDSProcessing(db)
    if err != nil {
        panic(err.Error())
    }
    fmt.Println("RDS処理 end!")
    return response, nil
}

/**************************
   メイン
**************************/
func main() {
    lambda.Start(run)
}

実行結果

上記の手順でRDS Proxyを用いての接続は完了です。(あれ意外と簡単)
Lambdaでの実行結果がこちらです。

スクリーンショット 2020-03-24 13.03.17.png

RDSから値が取得できました!

びっくりするのは応答時間!!!ご法度とされていたVPCLambdaですが、こんなにはやくなっているんです。これは使わない手はない!

おわりに

無事、LambdaからRDS Proxyを介してRDSに接続が可能になりました!これで同時接続数問題も気にしなくていい!!また、VPC内にLambdaを設置したときのコールドスタートが改善されたので、Lambdaを非VPCに設置しなくてもいけるし、RDSがプライベートサブネットに置ける!!素晴らしい!ただ、プレビュー版なのでお気をつけください。

次回こそはAPI Gatewayをたたいて、データをブラウザに表示します!

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

ゴリラ言語の読み方

先日、「ウ」と「ホ」と改行文字だけでプログラミングできる「ゴリラ言語」を作りました。

コード短いのでそのまま張っておきます。

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "strconv"
    "strings"
)

type Op int

const (
    Push Op = iota
    Signed
    Dup
    Copy
    Swap
    Discard
    Slide
    Add
    Sub
    Mul
    Div
    Mod
    Store
    Retrieve
    Mark
    Unsigned
    Call
    Jump
    Jz
    Jn
    Ret
    Exit
    OutC
    OutN
    InC
    InN
    Nop
)

type opcode struct {
    code Op
    arg  int
}

type opcodes []opcode

func (p *opcodes) jmp(arg int, pc *int) {
    for i, t := range *p {
        if t.code == Mark && t.arg == arg {
            *pc = i
        }
    }
}

var optable = map[string]Op{
    "  ":       Push,
    " \n ":     Dup,
    " \n\t":    Swap,
    " \n\n":    Discard,
    "\t   ":    Add,
    "\t  \t":   Sub,
    "\t  \n":   Mul,
    "\t \t ":   Div,
    "\t \t\t":  Mod,
    "\t\t ":    Store,
    "\t\t\t":   Retrieve,
    "\n  ":     Mark,
    "\n \t":    Call,
    "\n \n":    Jump,
    "\n\t ":    Jz,
    "\n\t\t":   Jn,
    "\n\t\n":   Ret,
    "\n\n\n":   Exit,
    "\t\n  ":   OutC,
    "\t\n \t":  OutN,
    "\t\n\t ":  InC,
    "\t\n\t\t": InN,
    //" \t ":     Copy,
    //" \t\n":    Slide,
}

type stack []int

func (s *stack) push(v int) {
    *s = append(*s, v)
}

func (s *stack) pop() int {
    l := len(*s)
    if l == 0 {
        return -1
    }
    v := (*s)[l-1]
    *s = (*s)[:l-1]
    return v
}

func (s *stack) dup() {
    l := len(*s)
    if l == 0 {
        return
    }
    v := (*s)[l-1]
    *s = append(*s, v)
}

func uho(src []rune) {
    for i, r := range src {
        switch r {
        case 'ウ':
            src[i] = ' '
        case 'ホ':
            src[i] = '\t'
        }
    }
    // remove needless comments
    for {
        pos := strings.IndexFunc(string(src), func(r rune) bool {
            return r != ' ' && r != '\t' && r != '\n'
        })
        if pos < 0 {
            break
        }
        if pos == 0 {
            src = src[1:]
        } else {
            src = append(src[:pos], src[pos+1:]...)
        }
    }

    // parse uho into tokens
    tokens := opcodes{}
    for len(src) > 0 {
        op := ""
        code := Nop
        for k, v := range optable {
            if strings.HasPrefix(string(src), k) {
                op = k
                code = v
                break
            }
        }
        if op == "" {
            src = src[1:]
            continue
        }
        src = src[len(op):]
        var arg int
        switch code {
        case Push:
            // handle argument
        handle_signed_arg:
            for i := 1; i < len(src); i++ {
                switch src[i] {
                case ' ':
                    arg = (arg << 1) | 0
                case '\t':
                    arg = (arg << 1) | 1
                case '\n':
                    // Push take singed argument
                    if src[0] == '\t' {
                        arg = -arg
                    }
                    src = src[i+1:]
                    break handle_signed_arg
                }
            }
        case Mark, Call, Jump, Jz, Jn:
            // handle argument
        handle_unsigned_arg:
            for i := 0; i < len(src); i++ {
                switch src[i] {
                case ' ':
                    arg = (arg << 1) | 0
                case '\t':
                    arg = (arg << 1) | 1
                case '\n':
                    src = src[i+1:]
                    break handle_unsigned_arg
                }
            }
        }
        tokens = append(tokens, opcode{code, arg})
    }

    pc := 0
    ps := stack{}
    cs := stack{}
    heap := map[int]int{}
    for {
        token := tokens[pc]

        code, arg := token.code, token.arg
        //fmt.Println(pc, code, arg)
        pc++
        switch code {
        case Push:
            ps.push(arg)
        case Mark:
        case Dup:
            ps.dup()
        case OutN:
            fmt.Print(ps.pop())
        case OutC:
            fmt.Print(string(rune(ps.pop())))
        case Add:
            rhs := ps.pop()
            lhs := ps.pop()
            ps.push(lhs + rhs)
        case Sub:
            rhs := ps.pop()
            lhs := ps.pop()
            ps.push(lhs - rhs)
        case Mul:
            rhs := ps.pop()
            lhs := ps.pop()
            ps.push(lhs * rhs)
        case Div:
            rhs := ps.pop()
            lhs := ps.pop()
            ps.push(lhs / rhs)
        case Mod:
            rhs := ps.pop()
            lhs := ps.pop()
            ps.push(lhs % rhs)
        case Jz:
            if ps.pop() == 0 {
                tokens.jmp(arg, &pc)
            }
        case Jn:
            if ps.pop() < 0 {
                tokens.jmp(arg, &pc)
            }
        case Jump:
            tokens.jmp(arg, &pc)
        case Discard:
            ps.pop()
        case Exit:
            os.Exit(0)
        case Store:
            v := ps.pop()
            address := ps.pop()
            heap[address] = v
        case Call:
            cs.push(pc)
            tokens.jmp(arg, &pc)
        case Retrieve:
            ps.push(heap[ps.pop()])
        case Ret:
            pc = cs.pop()
        case InC:
            var b [1]byte
            os.Stdin.Read(b[:])
            heap[ps.pop()] = int(b[0])
        case InN:
            scanner := bufio.NewScanner(os.Stdin)
            if scanner.Scan() {
                i, _ := strconv.Atoi(scanner.Text())
                heap[ps.pop()] = i
            }
        case Swap:
            ps[len(ps)-1], ps[len(ps)-2] = ps[len(ps)-2], ps[len(ps)-1]
        default:
            panic(fmt.Sprintf("Unknown opcode: %v", code))
        }
    }
}

func main() {
    var content bytes.Buffer

    for _, f := range os.Args[1:] {
        b, err := ioutil.ReadFile(f)
        if err != nil {
            fmt.Fprintln(os.Stderr, "read file:", err)
            os.Exit(1)
        }
        content.Write(b)
    }

    if content.Len() == 0 {
        if _, err := io.Copy(&content, os.Stdin); err != nil {
            fmt.Fprintln(os.Stderr, "read stdin:", err)
            os.Exit(1)
        }
    }

    uho([]rune(content.String()))
}

以下のコードをコマンドラインから標準入力に食わせると、FizzBuzz が実行されます。

ウウウホッ

ウウウウ
ウ
ウウ
ウウウウホホ
ホウホッホ
ホウウホ
ウウウホッウホ
ホウホッホ
ホウウホウ
ウ
ウホ
ウホッ
ウ
ウホウウ

ウウウホッ
ウウウホッウウウホホウ
ホ
ウウウウウホッホウホッウウホ
ホ
ウウ
ウホッウホホ
ウウウホッウホ
ホウホッホ
ホウウホウ

ウ
ウホウウ

ウウウホッウ
ウウウホウウウウホッウ
ホ
ウウウウウホッホホウホウホッ
ホ
ウウ
ウホウホッホ

ウ
ウホウウ

ウウウホホ
ウウウホッホホホウホッウ
ウ
ウホッ
ウウホ
ウウ
ホ

ウウウホッウウ
ウウウホウホッウ
ホ
ウウウウウホ
ホウウウウ
ウウウウホッホウウホッウホ
ホウウホ
ホホウウ
ウ


便利ですね。また以下のコードを食わせると「ウホッ!」と表示されます。

ウッウウ
ウウウホウホウ
ウウウッホッウウウウホ
ウウウッホホウウウウッホホウッウウウッホホ
ウウウホホウウウッウホホウホッホウホホ
ウウウッホッホウウウウッホウホウウホホウ

ウウホウウウッウーウウウウウホウッウウホウウーホウウ
ウ
ウ
ホウッホホホウホウウウッホウホウウ
ホ
ウウ
ウ
ホウウウッウウウウウウッホウウーウホウウホウウ

ウウホホホウホウウウホッウホウウ
ウ



勘のいい方はコード見なくてもこの説明だけで気付いたかもしれません。そう「whitespace」です。

https://ja.wikipedia.org/wiki/Whitespace

Whitespace(ホワイトスペース)は、プログラミング言語のひとつであり、またそれを動作させるインタプリタを指している。WhitespaceはGPLにより配布されている。実用言語ではない難解プログラミング言語のひとつ。
本来 "whitespace" とは「空白」や「余白」を意味する英単語である。多くの一般的なプログラミング言語では空白に相当する文字(スペース、タブ、言語によっては改行も)は他の言語要素間の区切りとして使われている。しかし、言語 Whitespace においてはプログラムは空白文字だけで構成される(それ以外の文字列はコメント扱いで無視される)。そのため、一見するとプログラムであることすらわからないという珍しい言語である。

ゴリラ言語は、空白を「ウ」、タブ文字を「ホ」に見立てた Esolang です。この言語で目的の文字を表示するコードをどうやって書いているか想像もつかないかもしれません。実は whitespace のコンパイラを作っておられる方がいます。

https://vii5ard.github.io/whitespace/

ウェブ上の IDE になっています。例えばここの Files の中にある nerd.wsa を開くと以下のコードが表示されます。

; Push the zero-terminated string
; onto stack in reverse order.

    push "Hello Nerd!\n"

; Loop while top of stack is non-zero.

print:
    dup
    jz end
    printc
    jmp print

; Graceful end
end:
    drop
    end

独自のアセンブラ言語になっていて、コンパイルすると whitespace のソースが出力されます。この Hello Nerd!ウホッ! 書き換えると空白とタブ文字と改行文字で作られる whitespace のソースコードが出来上がります。






















真っ白けですね。vim 等を使い、空白を「ウ」、タブ文字を「ホ」に置換してみます。

ウウウ
ウウウホウホウ
ウウウホホホホホホホホウウウウウウウホ
ウウウホホウウウウホホウウウウホホ
ウウウホホウウウウホホウホホウホホ
ウウウホホウウウウホウホウウホホウ

ウウホウウウウウウウウウホウウウホウウホウウ
ウ
ウ
ホウホホホウホウウウホウホウウ
ホ
ウウ
ウ
ホウウウウウウウウウホウウウホウウホウウ

ウウホホホウホウウウホウホウウ
ウ





読めそうな気がしてきましたね。読める気がしましたよね?

読んでみましょう。ゴリラ言語の仕様は以下の通り。

IMP
[ウ] スタック操作
[ホ][ウ] 演算
[ホ][ホ] ヒープアクセス
[LF] フロー制御
[ホ][LF] I/O

STACK OPERATION
[ウ] 数値:数値をスタックに積む
[LF][ウ]:スタックの一番上を複製する
[ホ][ウ] 数値:スタックのn番目をコピーして一番上に積む
[LF][ホ]:スタックの1番目と2番目を交換する
[LF][LF]:スタックの一番上の物を捨てる

CALCULATE
[ウ][ウ]:加算
[ウ][ホ]:引き算
[ウ][LF]:かけ算
[ホ][ウ]:割り算
[ホ][ホ]:剰余

始めはデータ部ですね。push ([ウ])に続く引数、0(なし), 10(1<<1, 2<<1, 5<<1), 65281(1<<1, 2<<1, 4<<1, 8<<1, 16<<1, 33<<1), 12483(略), 12507(略), 12454(略) を積んでいます。スタックですから取り出す時には逆から取り出されます。つまりこれは「12454, 12507, 12483, 65281, 10, 0」、文字に直すと「ウホッ!」です。続いて制御部

ウウホウウウウウウウウウホウウウホウウホウウ
ウ
ウ
ホウホホホウホウウウホウホウウ
ホ
ウウ
ウ
ホウウウウウウウウウホウウウホウウホウウ

ウウホホホウホウウウホウホウウ
ウ




マーク([LF][ウ][ウ])でラベル定義([ホ][ウ][ウ][ウ][ウ][ウ][ウ][ウ][ウ][ウ][ホ][ウ][ウ][ウ][ホ][ウ][ウ][ホ][ウ][ウ])、スタックから取り出し([ウ][LF][ウ])、ゼロ比較([LF][ホ][ウ])、0 であればラベル ホホホウホウウウホウホウウ にジャンプしています。0 でない場合は画面に取り出した値を文字として表示([ホ][LF][ウ])します。そしてループの最初に戻る為に ホウウウウウウウウウホウウウホウウホウウ へジャンプ([LF][ウ][LF])です。0 比較時のジャンプ先としてマーク([LF][ウ][ウ])でラベル定義([ホ][ホ][ホ][ウ][ホ][ウ][ウ][ウ][ホ][ウ][ホ][ウ][ウ])しています。

ループから出た後は、スタックの一番上を捨て([ウ][LF][LF])、プログラムを終了([LF][LF][LF])します。

簡単ですね!もうこれでいつ会社で「先輩、このゴリラ言語わからないので読んでもらえますか?」と聞かれても読めますよね。

ちなみにこれを実行すると以下の様に画面に表示されます。

ET48oE2UEAAPUn5.png

言ってしまえば whitespace のパクリ言語なのですが、2文字書き換えるだけでオレオレ言語が作れます。「ピ」と「ヨ」と改行でヒヨコ言語を作る事も出来ます。ぜひ楽しんで下さい。

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

API設計について調べたことのまとめ

API設計について、最近得た知見をまとめました。私がGoで開発しているのでGoベースの話になります。
間違いがあればコメントで教えていただければ幸いです。

参考資料

設計

設計とは

設計は、ユーザー目線で利便性を考える 「インターフェース設計」 と 開発者目線で利便性を考える 「アーキテクチャ設計」 があります。「インターフェース設計」で最近多くみられるのはREST APIと言われる設計方法が主流です。また、「アーキテクチャ設計」では、様々なアーキテクチャがあり、自分の開発しているAPIに適したアーキテクチャを使用します。

インターフェース設計

インターフェース設計をする上で考えるべきことは、外部の大多数の開発者向けに開発する LSUDs(Large Set of Unknown Developers) と ある特定のアプリケーションのための開発者のための SSKDs(Small Set of Known Developers) の二つのどちらかなのかを明確にすることです。LSUDsでは、RESTなAPI設計が主流です。しかし、SSKDsは、必ずしもRESTなAPI設計に従って開発する必要はありません。開発しているアプリケーションによってAPI設計を考える必要があります。SSKDsで重要なことは、1画面1リクエストを意識することです。

Rest (REpresentational State Transfer)

Rest設計については下記の記事が参考になりました。

翻訳: WebAPI 設計のベストプラクティス

アーキテクチャ設計

アーキテクチャには数多くの種類があります。そのため、どのアーキテクチャを採用するべきか見極めることが大変難しいです。
GopherCon 2018の記事(参考資料)では、このような悩みを持つユーザーのために、アーキテクチャが紹介されていました。
この記事で紹介されている最も簡単なアーキテクチャは、 Flat structureLayered、 Modular です。また、 Domain Driven Development(ドメイン駆動設計) や Hexagonal Architecture も紹介されています。この記事の他にも、 クリーンアーキテクチャオニオンアーキテクチャ などもあります。これらのアーキテクチャの特徴を簡単にまとめたいと思います。

Flat structure

このアーキテクチャは、簡素なAPIに適しているアーキテクチャだと思われます。パッケージ分をせずに全て package main で統一します。そのため規模が大きくなると読みにくいアーキテクチャになると思います。

Layered

このアーキテクチャは、機能ごとにディレクトリを分けています。そうすることで、packegeごとの役割を明確にすることができます。
問題点は、違うディレクトリ同士で値を共有したい場合にどうやって共有するかか問題になります。

Modular

このアーキテクチャは、ハンドラごとにディレクトリを分けています。部品ごとに分けて開発をするので、追加や修正が簡単にできることが良い点だと思います。

その他

他のアーキテクチャは、また別の記事で深くまとめようと思います。

開発しているAPIに合った設計の見つけ方

自分が開発しているAPIは、どのアーキテクチャに向いているのかを見つける方法があります。その方法は、機能が増えたり規模が大きくなったらどこが問題になるかを考えることです。注意点としては、自分のAPIにカッチリ当てはまるアーキテクチャはほとんど無いので、試行錯誤する必要があります。試行錯誤には、かなりの理解が必要なので、「様々なアーキテクチャを実際に使用すること」や「実際に使用されているコードを読むこと」が重要なのではないのかと考えています。

終わりに

記事を書いていて新しく発見した知識も多かったです。自分が使用していたディレクトリ分けは、Layeredアーキテクチャに似ていると感じました。多くのアーキテクチャを学び、適しているアーキテクチャを見つけられるようになりたいです!
ありがとうございました。

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

GoでEd25519+SHA256のアドレス導出してみる(Symbol,Catapult,RC5)

はじめに

catapult-serverのv0.9.3.1から、公開鍵の導出方法が変わりました。

https://github.com/nemtech/catapult-server/releases/tag/v0.9.3.1

いままでは、Ed25519+Keccak256だったのが、標準のEd25519に変わりました。

ということで、GoのEd25519ライブラリが使えるんじゃないかと思うので、やってみたいと思います。

秘密鍵から公開鍵とアドレスを導出する

こちらのNIP10を参考にやっていきます。

https://github.com/nemtech/NIP/blob/eef91708a05fc6ce24a903d742ff46682008a8b7/NIPs/nip-0010.md

公開鍵

NIP10に例示してある秘密鍵と公開鍵は、

private: 575dbb3062267eff57c970a336ebbc8fbcfe12c5bd3ed7bc11eb0481d7704ced
public:  2e834140fd66cf87b254a693a2c7862c819217b676d3943267156625e816ec6f

なので、これをやってみる。

package main

import "fmt"
import "encoding/hex"
import "crypto/ed25519"

// https://golang.org/pkg/crypto/ed25519/

func main() {
    seed3, _ := hex.DecodeString("575dbb3062267eff57c970a336ebbc8fbcfe12c5bd3ed7bc11eb0481d7704ced")
    priv3 := ed25519.NewKeyFromSeed(seed3)
    var pub3 interface{} = priv3.Public()
    var pub3a ed25519.PublicKey = pub3.(ed25519.PublicKey)
    fmt.Println(hex.EncodeToString(pub3a))
}

seed3に代入しているのが秘密鍵です。

実行結果

2e834140fd66cf87b254a693a2c7862c819217b676d3943267156625e816ec6f

できました。

NIP10にはオールゼロの秘密鍵も例示してあります。

private : 0000000000000000000000000000000000000000000000000000000000000000
public  : 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29

seed3にオールゼロを代行してみます。

実行結果

3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29

アドレス

次にアドレスです。NIP10には以下のものが例示されています。

public : 2e834140fd66cf87b254a693a2c7862c819217b676d3943267156625e816ec6f
address: TATNE7Q5BITMUTRRN6IB4I7FLSDRDWZA37JGO5UW

ハッシュ化アドレス

次にアドレスです。

NIP10によると、次の手順が必要です。

  1. 公開鍵をSHA3-256する
  2. それをRIPEMD160する
  3. ネットワークIDを付加する
  4. それをSHA3-256して先頭4バイトを取得する
  5. 3にそれを付加する

6.の「BASE32する」は次でやります。

package main

import (
    "encoding/hex"
    "fmt"
    "io"
)
import "golang.org/x/crypto/sha3"
import "golang.org/x/crypto/ripemd160"

// https://godoc.org/golang.org/x/crypto/sha3
// https://godoc.org/golang.org/x/crypto/ripemd160

func main() {
    var publicKey string = "2e834140fd66cf87b254a693a2c7862c819217b676d3943267156625e816ec6f"
    pubByte, _ := hex.DecodeString(publicKey)
    hash1 := sha3.Sum256(pubByte)
    rip := ripemd160.New()
    _, _ = io.WriteString(rip, string(hash1[:]))
    hash2 := rip.Sum(nil)
    prefix, _ := hex.DecodeString("98")
    rawAddress := make([]byte, 20)
    copy(rawAddress, hash2)
    rawAddress = append(rawAddress[:1], rawAddress[0:]...)
    rawAddress[0] = prefix[0]
    checksum := sha3.Sum256(rawAddress)
    rawAddress = append(rawAddress, checksum[:4]...)
    fmt.Println(hex.EncodeToString(rawAddress))
}

実行結果

9826d27e1d0a26ca4e316f901e23e55c8711db20dfd2677696

この段階ではNIP10と一致しているかははっきりとしません。次に行きます。

BASE32

先ほど導出したハッシュ化アドレスをBASE32エンコーディングします。

package main

import (
    "encoding/base32"
    "encoding/hex"
    "fmt"
)

func main() {
    address := "9826d27e1d0a26ca4e316f901e23e55c8711db20dfd2677696"
    addressByte, _ := hex.DecodeString(address)
    encode := base32.StdEncoding.EncodeToString(addressByte)
    fmt.Println(encode)
}

実行結果

TATNE7Q5BITMUTRRN6IB4I7FLSDRDWZA37JGO5UW

NIP10と同じものが導出できました。

おわりに

Goに存在する一般的なライブラリ類を使って、秘密鍵から公開鍵とアドレスを導出することができました。

ソースの書き方がメチャクチャなのは許してください。

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

Golangで、デザインパターン「Decorator」を学ぶ

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Decorator」を学ぶ"

今回は、Pythonで実装した”Decorator”のサンプルアプリをGolangで実装し直してみました。

■ Decorator(デコレータ・パターン)

Decoratorパターン(デコレータ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。

UML class and sequence diagram

W3sDesign_Decorator_Design_Pattern_UML.jpg

UML class diagram

decorator.png
(以上、ウィキペディア(Wikipedia)より引用)

□ 備忘録

オブジェクトにどんどんデコレーション(飾り付け)を施していくようなデザインパターンとのことです。
スポンジケーキに、クリームを塗り、イチゴを載せればストロベリーショートケーキになるような感じです。
Pythonプログラミングに携わっていると、よくお目にかかるやつですね。

■ "Decorator"のサンプルプログラム

実際に、Decoratorパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。

  • 文字列Hello, world.をそのまま表示する
  • 文字列Hello, world.の前後に、#文字を差し込んで表示する
  • 文字列Hello, world.の前後に、#文字を差し込んで、さらに、 枠線で囲んで表示する
  • 文字列HELLOを 枠線で囲んで、前後に、*文字を差し込んで、さらに、 枠線で2回囲んで、、、(以下、略)
$ go run Main.go 
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

/+-----------+/
/|+---------+|/
/||*+-----+*||/
/||*|HELLO|*||/
/||*+-----+*||/
/|+---------+|/
/+-----------+/

見た目、ハノイの塔みたいな表示になりました。

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Decorator

  • ディレクトリ構成
.
├── Main.go
└── decorator
    ├── border.go
    └── display.go

(1) Componentの役

機能を追加するときの核に役です。Component役は。スポンジケーキのインタフェースだけを定めます。
サンプルプログラムでは、display構造体と、displayInterfaceインタフェースが、この役を努めます。

decorator/display.go
package decorator

import "fmt"

type displayInterface interface {
    getColumns() int
    getRows() int
    getRowText(row int) string
}

type display struct {
    myDisplay displayInterface
}

// Show func for printing something
func (d *display) Show() {
    str := ""
    for i := 0; i < d.myDisplay.getRows(); i++ {
        str += d.myDisplay.getRowText(i) + "\n"
    }
    fmt.Printf("%s", str)
}

... (snip)

(2) ConcreteComponentの役

Component役のインタフェースを実装している具体的なスポンジケーキです。
サンプルプログラムでは、StringDisplay構造体が、この役を努めます。

decorator/display.go
// StringDisplay is struct
type StringDisplay struct {
    *display
    String string
}

// NewStringDisplay func for initalizing StringDisplay
func NewStringDisplay(str string) *StringDisplay {
    stringDisplay := &StringDisplay{
        display: &display{},
        String:  str,
    }
    stringDisplay.myDisplay = stringDisplay
    return stringDisplay
}

func (s *StringDisplay) getColumns() int {
    return len(s.String)
}

func (s *StringDisplay) getRows() int {
    return 1
}

func (s *StringDisplay) getRowText(row int) string {
    if row == 0 {
        return s.String
    }
    return ""
}

(3) Decorator(装飾者)の役

Component役と同じインタフェースを持ちます。そして、さらに、このDecorator役が飾る対象となるComponent役を持っています。この役は、自分が飾っている対象を「知っている」わけです。
サンプルプログラムでは、border構造体が、この役を努めます。

decorator/border.go
package decorator

type border struct {
    *display
    neighorDisplay displayInterface
}

... (snip)

(4) ConcreteDecorator(具体的な装飾者)の役

具体的なDecoratorの役です。
サンプルプログラムでは、SideBorder構造体と、FullBorder構造体が、この役を努めます。

decorator/border.go
// SideBorder is struct
type SideBorder struct {
    *border
    borderChar string
}

// NewSideBorder func for initalizing SideBorder
func NewSideBorder(displayIf displayInterface, borderChar string) *SideBorder {
    sideBorder := &SideBorder{
        border: &border{
            display:        &display{},
            neighorDisplay: displayIf,
        },
        borderChar: borderChar,
    }
    sideBorder.myDisplay = sideBorder
    return sideBorder
}

func (s *SideBorder) getColumns() int {
    return len(s.borderChar)*2 + s.neighorDisplay.getColumns()
}

func (s *SideBorder) getRows() int {
    return s.neighorDisplay.getRows()
}

func (s *SideBorder) getRowText(row int) string {
    return s.borderChar + s.neighorDisplay.getRowText(row) + s.borderChar
}
decorator/border.go
// FullBorder is struct
type FullBorder struct {
    *SideBorder
}

// NewFullBorder func for initalizing SideBorder
func NewFullBorder(displayIf displayInterface) *FullBorder {
    fullBorder := &FullBorder{
        SideBorder: &SideBorder{
            border: &border{
                display:        &display{},
                neighorDisplay: displayIf}},
    }
    fullBorder.myDisplay = fullBorder
    return fullBorder
}

func (f *FullBorder) getColumns() int {
    return 2 + f.neighorDisplay.getColumns()
}

func (f *FullBorder) getRows() int {
    return 2 + f.neighorDisplay.getRows()
}

func (f *FullBorder) getRowText(row int) string {
    if row == 0 {
        return "+" + f.makeLine("-", f.neighorDisplay.getColumns()) + "+"
    } else if row == f.neighorDisplay.getRows()+1 {
        return "+" + f.makeLine("-", f.neighorDisplay.getColumns()) + "+"
    } else {
        return "|" + f.neighorDisplay.getRowText(row-1) + "|"
    }
}

func (f *FullBorder) makeLine(char string, count int) string {
    buf := ""
    for i := 0; i < count; i++ {
        buf += char
    }
    return buf
}

(5) Client(依頼人)の役

サンプルプログラムでは、startMain関数が、この役を努めます。

Main.go
package main

import (
    "fmt"

    "./decorator"
)

func startMain() {
    b1 := decorator.NewStringDisplay("Hello, world.")
    b2 := decorator.NewSideBorder(b1, "#")
    b3 := decorator.NewFullBorder(b2)
    b1.Show()
    b2.Show()
    b3.Show()
    fmt.Println("")
    b4 := decorator.NewSideBorder(
        decorator.NewFullBorder(
            decorator.NewFullBorder(
                decorator.NewSideBorder(
                    decorator.NewFullBorder(
                        decorator.NewStringDisplay("HELLO"),
                    ),
                    "*",
                ),
            ),
        ),
        "/",
    )
    b4.Show()
}

func main() {
    startMain()
}

■ 参考URL

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

Golangで、デザインパターン「Composite」を学ぶ

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Composite」を学ぶ"

今回は、Pythonで実装した”Composite”のサンプルアプリをGolangで実装し直してみました。

■ Composite(コンポジット・パターン)

Compositeパターン(コンポジット・パターン)とは、GoF (Gang of Four; 4人のギャングたち) によって定義された デザインパターンの1つである。「構造に関するパターン」に属する。Compositeパターンを用いるとディレクトリとファイルなどのような、木構造を伴う再帰的なデータ構造を表すことができる。
Composite パターンにおいて登場するオブジェクトは、「枝」と「葉」であり、これらは共通のインターフェースを実装している。そのため、枝と葉を同様に扱えるというメリットがある。

UML class and object diagram

W3sDesign_Composite_Design_Pattern_UML.jpg

UML class diagram

composite.png
(以上、ウィキペディア(Wikipedia)より引用)

□ 備忘録

書籍「増補改訂版Java言語で学ぶデザインパターン入門」の引用ですが、腹落ちしました。

ディレクトリの中には、ファイルが入っていたり、別のディレクトリ(サブディレクトリ)が入っていたりします。そしてまた、そのサブディレクトリの中には他のファイルやサブディレクトリが入っていることもあります。
ディレクトリは、そのような「入れ子」になった構造、再帰的な構造を作り出しています。
... (snip)
Compositeパターンは、このような構造を作るためのものであり、容器と中身を同一視し、再帰的な構造を作るデザインパターン

■ "Composite"のサンプルプログラム

実際に、Compositeパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。

  • ルートエントリのディレクトリに、サブディレクトリおよびファイルを追加してみる
  • ルートエントリのディレクトリに、ユーザエントリのディレクトリを追加して、さらに、 サブディレクトリおよびファイルを追加してみる
  • 敢えて、ファイルに、ディレクトリを追加して、失敗することを確認する
$ go run Main.go 
Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)

Making user entries...
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/Composite.java (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.tex (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)

Occurring Exception...
FileTreatmentException

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Composite

  • ディレクトリ構成
.
├── Main.go
└── composite
    ├── directory.go
    ├── entry.go
    └── file.go

(1) Leaf(葉)の役

「中身」を表す役です。この役の中には、他のものを入れることができません。
サンプルプログラムでは、File構造体が、この役を努めます。

composite/file.go
package composite

import (
    "fmt"
)

// File is sturct
type File struct {
    name string
    size int
}

// NewFile func for initializing File
func NewFile(name string, size int) *File {
    return &File{
        name: name,
        size: size,
    }
}

func (f *File) getName() string {
    return f.name
}

func (f *File) getSize() int {
    return f.size
}

// Add func for adding file
func (f *File) Add(entry entry) {
    if err := doError(); err != nil {
        fmt.Println(err)
    }
}

// PrintList func for printing directory name
func (f *File) PrintList(prefix string) {
    f.print(prefix)
}

func (f *File) print(prefix string) {
    fmt.Printf("%s/%s (%d)\n", prefix, f.getName(), f.getSize())
}

func doError() error {
    msg := "FileTreatmentException"
    return fmt.Errorf("%s", msg)
}

(2) Composite(複合体)の役

「容器」を表す役です。Leaf役や、Composite役を入れることができます。
サンプルプログラムでは、Directory構造体が、この役を努めます。

composite/directory.go
package composite

import "fmt"

// Directory is sturct
type Directory struct {
    name      string
    directory []entry
}

// NewDirectory func for initializing Directory
func NewDirectory(name string) *Directory {
    return &Directory{
        name: name,
    }
}

func (d *Directory) getName() string {
    return d.name
}

func (d *Directory) getSize() int {
    size := 0
    for _, entry := range d.directory {
        size += entry.getSize()
    }
    return size
}

// Add func for adding directory
func (d *Directory) Add(entry entry) {
    d.directory = append(d.directory, entry)
}

// PrintList func for printing directory name
func (d *Directory) PrintList(prefix string) {
    d.print(prefix)
    for _, e := range d.directory {
        e.PrintList(prefix + "/" + d.name)
    }
}

func (d *Directory) print(prefix string) {
    fmt.Printf("%s/%s (%d)\n", prefix, d.getName(), d.getSize())
}

(3) Componentの役

Leaf役とComposite役を同一視するための役です。
サンプルプログラムでは、Entryインタフェースが、この役を努めます。

composite/entry.go
package composite

type entry interface {
    PrintList(prefix string)
    getSize() int
}

(4) Client(依頼人)の役

サンプルプログラムでは、startMain関数が、この役を努めます。

Main.go
package main

import (
    "fmt"

    "./composite"
)

func startMain() {
    fmt.Println("Making root entries...")
    rootdir := composite.NewDirectory("root")
    bindir := composite.NewDirectory("bin")
    tmpdir := composite.NewDirectory("tmp")
    usrdir := composite.NewDirectory("usr")

    rootdir.Add(bindir)
    rootdir.Add(tmpdir)
    rootdir.Add(usrdir)

    bindir.Add(composite.NewFile("vi", 10000))
    bindir.Add(composite.NewFile("latex", 20000))
    rootdir.PrintList("")

    fmt.Println("")
    fmt.Println("Making user entries...")
    yuki := composite.NewDirectory("yuki")
    hanako := composite.NewDirectory("hanako")
    tomura := composite.NewDirectory("tomura")

    usrdir.Add(yuki)
    usrdir.Add(hanako)
    usrdir.Add(tomura)

    yuki.Add(composite.NewFile("diary.html", 100))
    yuki.Add(composite.NewFile("Composite.java", 200))
    hanako.Add(composite.NewFile("memo.tex", 300))
    tomura.Add(composite.NewFile("game.doc", 400))
    tomura.Add(composite.NewFile("junk.mail", 500))
    rootdir.PrintList("")

    fmt.Println("")
    fmt.Println("Occurring Exception...")
    tmpfile := composite.NewFile("junk.mail", 500)
    bindir = composite.NewDirectory("bin")
    tmpfile.Add(bindir)
}

func main() {
    startMain()
}

(5) その他

エラー時の振る舞いを追加します

composite/file.go
func doError() error {
    msg := "FileTreatmentException"
    return fmt.Errorf("%s", msg)
}

■ 参考URL

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