20200625のGoに関する記事は6件です。

goでデザインパターン

builder

任意の項目を指定して構造体を初期化するパターン
各項目の関数では自身を返し、メソッドチェインで構造体を初期化していく。

package main

type email struct {
    from, to, subject, body string
}
type EmailBuilder struct {
    email email
}
func (b *EmailBuilder) From(from string) *EmailBuilder {
    b.email.from = from
    return b
}
func (b *EmailBuilder) To(to string) *EmailBuilder {
    b.email.to = to
    return b
}
func (b *EmailBuilder) Subject(subject string) *EmailBuilder {
    b.email.subject = subject
    return b
}
func (b *EmailBuilder) Body(body string) *EmailBuilder {
    b.email.body = body
    return b
}
func sendMail(email *email) {
    // 実際の処理
}
type build func(*EmailBuilder)
func SendEmail(action build) {
    builder := EmailBuilder{}
    action(&builder)
    sendMail(&builder.email)
}
func main() {
    SendEmail(func(b *EmailBuilder) {
        b.From("hoge@example.com").
            To("huga@example.com").
            Subject("Sample").
            Body("HOGE")
    })
}

functional builder

任意の項目を指定して構造体を初期化するパターン。
上述のbuilderパターンの関数バージョン。

package main

type email struct {
    from, to, subject, body string
}

type EmailBuilder struct {
    email email
}

type Option func(b *EmailBuilder)

func From(from string) Option {
    return func(b *EmailBuilder) {
        b.email.from = from
    }
}
func To(to string) Option {
    return func(b *EmailBuilder) {
        b.email.to = to
    }
}
func Subject(subject string) Option {
    return func(b *EmailBuilder) {
        b.email.subject = subject
    }
}
func Body(body string) Option {
    return func(b *EmailBuilder) {
        b.email.body = body
    }
}
func sendMail(email *email) {
    // 実際の処理
}

func SendEmail(ops ...Option) {
    builder := EmailBuilder{}

    for _, option := range ops {
        option(&builder)
    }
    sendMail(&builder.email)
}

func main() {
    SendEmail(From("hoge@example.com"),
        To("huga@example.com"),
        Subject("Sample"),
        Body("HOGE"))
}

factory

構造体の初期化をfactoryに移譲するパターン。
各factoryで初期化される構造体は別物だが、同じような振る舞いを持つ構造体であれば、下記のようにinterfaceを利用し、createメソッドを抽象化すれば、疎結合にできる。

package main

import "fmt"

type Employee struct {
    Name, Position string
}

func (e *Employee) setName(name string) {
    e.Name = name
}

type NewEmployeeFactory interface {
    create() *Employee
}

type DeveloperFactory struct {}
func (DeveloperFactory) create() *Employee {
    return &Employee{"", "Developer"}
}

type ManagerFactory struct {}
func (ManagerFactory) create() *Employee {
    return &Employee{"", "Manager"}
}

func NewEmployee(factory NewEmployeeFactory) *Employee {
    return factory.create()
}
func main() {
    developerFactory := DeveloperFactory{}
    dev := NewEmployee(developerFactory)
    dev.setName("Sam")
    managerFactory := ManagerFactory{}
    manager := NewEmployee(managerFactory)
    manager.setName("Bob")

    fmt.Println(dev)
    fmt.Println(manager)
}

decorator

構造体にいろんな機能をデコレーションするかのように追加できるパターン。
構造体の中にinterfaceがあるのがポイント。interfaceを満たしていれば、デコレートし続けることができる

package main
import "fmt"

type Shape interface {
    Render() string
}
type Circle struct {
    Radius float32
}
func (c *Circle) Render() string {
    return fmt.Sprintf("Circle of radius %f",
        c.Radius)
}

type ColoredShape struct {
    Shape Shape
    Color string
}
func (c *ColoredShape) Render() string {
    return fmt.Sprintf("%s has the color %s",
        c.Shape.Render(), c.Color)
}
type TransparentShape struct {
    Shape Shape
    Transparency float32
}
func (t *TransparentShape) Render() string {
    return fmt.Sprintf("%s has %f%% transparency",
        t.Shape.Render(), t.Transparency * 100.0)
}

func main() {
    circle := Circle{2}
    fmt.Println(circle.Render())
    redCircle := ColoredShape{&circle, "Red"}
    fmt.Println(redCircle.Render())
    rhsCircle := TransparentShape{&redCircle, 0.5}
    fmt.Println(rhsCircle.Render())
}

state

状態を構造体として表すパターン。

  • On/Offの状態を構造体で持つ。On構造体はOffメソッド、Off構造体はOnメソッドを実装している。
  • Offの状態の時のOffメソッド、Onの状態の時のOnメソッドの実行は、BaseState構造体のOn/Offメソッドでカバー。
  • 状態を構造体で表し、状態遷移をメソッドで表すことで、if文を使うことなく、stateを扱えている。
package main
import "fmt"

type Switch struct {
    State State
}
func NewSwitch() *Switch {
    return &Switch{NewOffState()}
}
func (s *Switch) On() {
    s.State.On(s)
}
func (s *Switch) Off() {
    s.State.Off(s)
}
type State interface {
    On(sw *Switch)
    Off(sw *Switch)
}
type BaseState struct {}
func (s *BaseState) On(sw *Switch) {
    fmt.Println("Light is already on")
}
func (s *BaseState) Off(sw *Switch) {
    fmt.Println("Light is already off")
}
type OnState struct {
    BaseState
}
func NewOnState() *OnState {
    fmt.Println("Light turned on")
    return &OnState{BaseState{}}
}
func (o *OnState) Off(sw *Switch) {
    fmt.Println("Turning light off...")
    sw.State = NewOffState()
}
type OffState struct {
    BaseState
}
func NewOffState() *OffState {
    fmt.Println("Light turned off")
    return &OffState{BaseState{}}
}
func (o *OffState) On(sw *Switch) {
    fmt.Println("Turning light on...")
    sw.State = NewOnState()
}
func main() {
    sw := NewSwitch()
    sw.On()
    sw.Off()
    sw.Off()
}

strategy

機能の切り替えを容易にするパターン。
似たような機能が複数あったとして、その各機能のinterfaceを揃えてあげれば、具象的な処理を切り替えるだけでストラテジパターンが作れる。

  • MarkdownListStrategy、HtmlListStrategyから生えてるメソッドでmarkdown、htmlの出力がされる。
  • MarkdownListStrategy、HtmlListStrategyはListStrategyのinterfaceを満たしている。
  • MarkdownListStrategy、HtmlListStrategyのどちらを使うかはSetOutputFormatで切り替えれる。
  • AppendListメソッドでbufferに出力内容をためていく。二つのStrategyがListStrategyを満たしているため、ここの処理は共通化できる。
package main
import (
    "fmt"
    "strings"
)
type OutputFormat int
const (
    Markdown OutputFormat = iota
    Html
)
type ListStrategy interface {
    Start(builder *strings.Builder)
    End(builder *strings.Builder)
    AddListItem(builder *strings.Builder, item string)
}
type MarkdownListStrategy struct {}
func (m *MarkdownListStrategy) Start(builder *strings.Builder) {}
func (m *MarkdownListStrategy) End(builder *strings.Builder) {}
func (m *MarkdownListStrategy) AddListItem(
    builder *strings.Builder, item string) {
    builder.WriteString(" * " + item + "\n")
}
type HtmlListStrategy struct {}
func (h *HtmlListStrategy) Start(builder *strings.Builder) {
    builder.WriteString("<ul>\n")
}
func (h *HtmlListStrategy) End(builder *strings.Builder) {
    builder.WriteString("</ul>\n")
}
func (h *HtmlListStrategy) AddListItem(builder *strings.Builder, item string) {
    builder.WriteString("  <li>" + item + "</li>\n")
}
type TextProcessor struct {
    builder strings.Builder
    listStrategy ListStrategy
}
func NewTextProcessor(listStrategy ListStrategy) *TextProcessor {
    return &TextProcessor{strings.Builder{}, listStrategy}
}
func (t *TextProcessor) SetOutputFormat(fmt OutputFormat) {
    switch fmt {
    case Markdown:
        t.listStrategy = &MarkdownListStrategy{}
    case Html:
        t.listStrategy = &HtmlListStrategy{}
    }
}
func (t *TextProcessor) AppendList(items []string) {
    t.listStrategy.Start(&t.builder)
    for _, item := range items {
        t.listStrategy.AddListItem(&t.builder, item)
    }
    t.listStrategy.End(&t.builder)
}
func (t *TextProcessor) Reset() {
    t.builder.Reset()
}
func (t *TextProcessor) String() string {
    return t.builder.String()
}
func main() {
    tp := NewTextProcessor(&MarkdownListStrategy{})
    tp.AppendList([]string{ "foo", "bar", "baz" })
    fmt.Println(tp)
    tp.Reset()
    tp.SetOutputFormat(Html)
    tp.AppendList([]string{ "foo", "bar", "baz" })
    fmt.Println(tp)
}

proxy

処理の前にある処理を挟みたい場合のパターン。interfaceの差異の吸収だったり、バリデーションだったり。
実際に実行したいメソッド名とプロキシのメソッド名は合わせる。利用者側にはプロキシが挟まっていることを意識させない。

package proxy
import "fmt"

type Driven interface {
  Drive()
}
type Car struct {}
func (c *Car) Drive() {
  fmt.Println("Car being driven")
}
type Driver struct {
  Age int
}
type CarProxy struct {
  car Car
  driver *Driver
}
func (c *CarProxy) Drive() {
  if c.driver.Age >= 18 {
    c.car.Drive()
  } else {
    fmt.Println("Driver too young")
  }
}
func NewCarProxy(driver *Driver) *CarProxy {
  return &CarProxy{Car{}, driver}
}
func main() {
  car := NewCarProxy(&Driver{12})
  car.Drive()
}

再帰的な構造体

構造体の中にinterfaceを定義することで、再帰的な構造体が作れる。

  • AdditionalExpressionとDoubleExpressionはleft/rightにExpressionのinterfaceを持つ。
  • AdditionalExpressionとDoubleExpression自体がExpressionのinterfaceを満たしているので、left/rightを再帰的にすることができる
package main
import (
    "fmt"
    "strings"
)
type Expression interface {
    Print(sb *strings.Builder)
}
type DoubleExpression struct {
    value float64
}
func (d *DoubleExpression) Print(sb *strings.Builder) {
    sb.WriteString(fmt.Sprintf("%g", d.value))
}
type AdditionExpression struct {
    left, right Expression
}
func (a *AdditionExpression) Print(sb *strings.Builder) {
    sb.WriteString("(")
    a.left.Print(sb)
    sb.WriteString("+")
    a.right.Print(sb)
    sb.WriteString(")")
}
func main() {
    e := AdditionExpression{
        &DoubleExpression{1},
        &AdditionExpression{
            left:  &DoubleExpression{2},
            right: &DoubleExpression{3},
        },
    }
    sb := strings.Builder{}
    e.Print(&sb)
    fmt.Println(sb.String()) // 1+(2+3)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】簡易 fuzzylauncher

以前の記事: 【PowerShell】 peco を使った簡易ランチャ

最初はうまく動作してはしゃいでていたものの、なんとなくモッサリした動作が気になってきた今日この頃。勉強も兼ねて go で書き直してみました。

最終的にこのような感じになります。
202006262125035.png

制作過程

まずは Test-Path に相当するファイルの有無を確認する関数。

func isVaridPath(filename string) bool {
    _, err := os.Stat(filename)
    return err == nil
}

次は Get-Content の代わりのテキストファイル読み取り関数。
先に make しておくとベターとの記事を見かけたので組み込んでますが容量はどの値を指定すればいいのでしょうか……(勉強不足)。とりあえずキリの良い100で。

func readFile(filePath string) []string {
    f, err := os.Open(filePath)
    if err != nil {
        panic(err)
    }
    defer f.Close()

    lines := make([]string, 0, 100)
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        panic(err)
    }

    return lines
}

苦労したのは Get-Childitem -depth を go で実装する処理。
最初は filepath.Walk を使っていたのですが、 godirwalk というライブラリが探索の速さを謳っていたので我流で組み込んでいます。
こちらも容量の値はよく理解しないまま1000を指定。理由はテキストファイルから読み込むよりも確実にスライスが長くなるからです。そしてポインタにはまだ理解が及んでいないので写経状態。

func dirWalkDepth(dirname string, depth int) []string {
    dirs := make([]string, 0, 1000)
    err := godirwalk.Walk(dirname, &godirwalk.Options{
        FollowSymbolicLinks: false,
        Callback: func(osPathname string, de *godirwalk.Dirent) error {
            rel, _ := filepath.Rel(dirname, osPathname)
            if strings.Count(rel, `\`) >= depth {
                return filepath.SkipDir
            }
            if de.IsDir() {
                if strings.HasPrefix(de.Name(), ".") {
                    return filepath.SkipDir
                }
                dirs = append(dirs, osPathname)
            }
            return nil
        },
        ErrorCallback: func(osPathname string, err error) godirwalk.ErrorAction {
            fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
            return godirwalk.SkipNode
        },
    })
    if err != nil {
        panic(err)
    }
    return dirs
}

最後はメインのフィルタリング処理。ライブラリとしての fuzzy-finder が提供されていました。大変ありがたいことです。

実装はドキュメントのサンプルをほぼコピペ。
fuzzyfinder.Find で呼んだ関数の戻り値が実際に表示されるようなので、スライスの要素に | が含まれていればその後ろ、そうでなければ filepath.Base でパス末端を取得します。 Escape や Ctrl-C でキャンセルされた場合は err が戻ります。

    idx, err := fuzzyfinder.Find(rootSlice, func(i int) string {
        sliceItem := rootSlice[i]
        if strings.Contains(sliceItem, "|") {
            return strings.Split(sliceItem, "|")[1]
        }
        return filepath.Base(sliceItem)
    })
    if err != nil {
        return
    }

コード全体

fuzzylauncher.go
package main

import (
    "bufio"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "sort"
    "strings"

    "github.com/karrick/godirwalk"
    "github.com/ktr0731/go-fuzzyfinder"
)

const (
    dataPath = `C:\Personal\launch.txt`
)

func main() {
    if !isVaridPath(dataPath) {
        fmt.Println("cannot find data file...")
        return
    }
    rootSlice := []string{}
    for _, p := range readFile(dataPath) {
        if isVaridPath(strings.Split(p, "|")[0]) {
            rootSlice = append(rootSlice, p)
        }
    }
    sort.Slice(rootSlice, func(i, j int) bool {
        return filepath.Base(rootSlice[i]) < filepath.Base(rootSlice[j])
    })
    idx, err := fuzzyfinder.Find(rootSlice, func(i int) string {
        sliceItem := rootSlice[i]
        if strings.Contains(sliceItem, "|") {
            return strings.Split(sliceItem, "|")[1]
        }
        return filepath.Base(sliceItem)
    })
    if err != nil {
        return
    }
    src1 := strings.Split(rootSlice[idx], "|")[0]
    if f, _ := os.Stat(src1); !f.IsDir() {
        exec.Command("cmd", "/c", "start", "", src1).Start()
        return
    }
    subDir := dirWalkDepth(src1, 4)
    src2 := subDir[0]
    if len(subDir) > 1 {
        idx, err := fuzzyfinder.Find(subDir, func(i int) string {
            relPath, _ := filepath.Rel(src1, subDir[i])
            return relPath
        })
        if err != nil {
            return
        }
        src2 = subDir[idx]
    }
    exec.Command("explorer.exe", src2).Start()
}

func isVaridPath(filename string) bool {
    _, err := os.Stat(filename)
    return err == nil
}

func readFile(filePath string) []string {
    f, err := os.Open(filePath)
    if err != nil {
        panic(err)
    }
    defer f.Close()

    lines := make([]string, 0, 100)
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        panic(err)
    }

    return lines
}

func dirWalkDepth(dirname string, depth int) []string {
    dirs := make([]string, 0, 1000)
    err := godirwalk.Walk(dirname, &godirwalk.Options{
        FollowSymbolicLinks: false,
        Callback: func(osPathname string, de *godirwalk.Dirent) error {
            rel, _ := filepath.Rel(dirname, osPathname)
            if strings.Count(rel, `\`) >= depth {
                return filepath.SkipDir
            }
            if de.IsDir() {
                if strings.HasPrefix(de.Name(), ".") {
                    return filepath.SkipDir
                }
                dirs = append(dirs, osPathname)
            }
            return nil
        },
        ErrorCallback: func(osPathname string, err error) godirwalk.ErrorAction {
            fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
            return godirwalk.SkipNode
        },
    })
    if err != nil {
        panic(err)
    }
    return dirs
}

感想

コピー&ペーストでできたフランケンシュタインの怪物のようなコードですが動いているので満足です。
はじめての go でした。コンパイルが通りさえすれば大抵は問題なく動作するというのは安心感がありますね。

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

Hyperledger Fabric [asset]ソースコードとダミーデータをgitへ上げました

共有する

Goソースコード、Shellスクリプト、ダミーデータ(1万レコード)、PostgreSQLのyamlファイルをgitへアップロードしました。
https://github.com/settembre21/asset

以下の投稿に関連するものです。
Hyperledger FabricのKeyゾンビ化を防ぐ(ソースコード掲載)
Hyperledger Fabricで個人情報をクエリする(RDB連携)
Hyperledger FabricのGetHistoryForKeyを利用した資産のトレース
Hyperledger FabricのState DBをPostgreSQLを使って初期化する

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

モダンな言語のバイナリサイズとコンパイル時間について調べてみた

モダンなコンパイル言語のバイナリサイズとコンパイル時間を計測してみました。
比較のために C と C++ も含まれています。

言語 バイナリサイズ コンパイル時間 依存ライブラリ
Nim 61,908 0m0.720s /usr/lib/libSystem.B.dylib
V 27,276 0m1.526s /usr/lib/libSystem.B.dylib
Go 1,691,864 0m0.295s /usr/lib/libSystem.B.dylib
Rust 14,440 0m1.284s @rpath/libstd-91fba10bbd33db69.dylib
/usr/lib/libSystem.B.dylib
/usr/lib/libresolv.9.dylib
Haskell 18,332 0m1.915s /usr/lib/libSystem.B.dylib
@rpath/libHSbase-4.12.0.0-ghc8.6.5.dylib
@rpath/libHSghc-prim-0.5.3-ghc8.6.5.dylib
@rpath/libHSrts-ghc8.6.5.dylib
C 12,816 0m0.083s /usr/lib/libSystem.B.dylib
C++ 29,836 0m0.694s /usr/lib/libc++.1.dylib
/usr/lib/libSystem.B.dylib

確認に使用したプラットフォームは macOS Catalina です。

比較に使用したのは自分自身のソースコードをファイルから読み取り、標準出力に表示するプログラムです。まったく同等の処理ではないかも知れないので、参考程度に見てください。各言語のコンパイルは速度優先で最適化しています。

感想

C のコンパイル時間の速さは圧巻です。

C++ のコンパイルは遅いとよく言われるのですが、今回のケースでは V、Rust、Haskell はそれよりもかなり遅いです。ただし、Rust と Haskell は動的リンクオプションをやめて静的リンクにすると、Rust は Go ぐらいまで、Haskell は C++ ぐらいまで速くなります(なぜ静的リンクは動的リンクよりコンパイルが速いのか、については私はわかっていません)。

Nim と V は C へのトランスパイラで、それぞれの言語のランタイムも C へトランスパイルされてバイナリに含まれます(という理解です)。V はかなり小さなバイナリを出力しています。
Nim は言語機能としてガベージコレクションを持っており、当然ランタイムにもガベージコレクションのコードが含まれるとすると、このサイズは大健闘だと思います。ただ、V(や Rust)はコンパイル時にオブジェクトの開放のタイミングを決定し自動開放してくれるという、GC とはまた違った強みがあります。

Go のバイナリが極端に大きいのは、Go のランタイムを含む外部ライブラリをすべて静的リンクしているためです。本当の意味でのシングルバイナリでポータビリティを担保しているのは Go だけ(という認識)です。

以下、検証したコードと一言コメントです。

Nim

cat_nim.nim
let s = readFile("cat_nim.nim")
echo s

Nim のコードは本当に簡単です。

V

cat_v.v
import os

s := os.read_file('cat_v.v')?
println(s)

V も簡単です。read_file がちゃんとモジュールの関数になっているのが好みです。

Go

cat_go.go
package main

import (
  "fmt"
  "io/ioutil"
)

func main() {
  s, err := ioutil.ReadFile("cat_go.go")
  if err != nil {
    panic(err)
  }
  fmt.Println(string(s))
}

ちょっとコード量が多い印象です。

Rust

cat_rust.rs
use std::fs;

fn main() {
  let contents = fs::read_to_string("cat_rust.rs").expect("error");
  println!("{}", contents);
}

Rust はエラー処理を省略すると結構短く書けます。

Haskell

cat_haskell.hs
main = readFile "cat_haskell.hs" >>= putStrLn

1 行で書けました!

C

cat_c.c
#include <stdio.h>

int main() {
  char str[256];
  FILE *fp = fopen("cat_c.c", "r");
  while (fgets(str, sizeof(str), fp) != NULL) {
    printf("%s", str);
  }
  printf("\n");
  fclose(fp);
  return 0;
}

バッファを用意してそこに繰り返し読み込んで、とやっていることは原始的です。が、これが基本にして最速なんですね。

C++

cat_cpp.cpp
#include <fstream>
#include <iostream>

int main() {
  std::string s;
  std::ifstream f("cat_cpp.cpp");
  while (std::getline(f, s)) {
    std::cout << s << std::endl;
  }
  std::cout << std::endl;
  f.close();
  return 0;
}

C++ のライブラリを使っていますが、やっていることはほとんど C と変わらないです。

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

Goで数値型配列の中身を高い順にsort(並び替え)する方法

はじめに

Goで数値型配列の中身を高い順にsortしてみました。

コード

main.go
package main

import (
    "fmt"
    "sort"
)

func main() {

    slice := []int{1,4,3,5,6,19,21,7,8,10,49}

        sort.Sort(sort.Reverse(sort.IntSlice(slice)))これがPoint
        fmt.Println(slice)

}
// 出力結果
[49 21 19 10 8 7 6 5 4 3 1]

AtCoderなんかの問題を解くときに使ったときは

main.go
package main

import (
    "fmt"
    "sort"
)

func main() {
        var A int
        slice := []int{}

        for i := 0; i < 10; i++ {
        fmt.Scan(&A)
        slice = append(slice, A)
    }
        sort.Sort(sort.Reverse(sort.IntSlice(slice)))
        fmt.Println(slice)

}
$ go run main.go
89
23
4
67
100
200
555
2
1
0
// 出力結果
[555 200 100 89 67 23 4 2 1 0]

こういった形で使用。

アプリ開発のとき等によくある「1つずつ中身を取り出したい」というときはrangeを使って

main.go
for _, s := range slice {
        fmt.Println(s)
    }

のようにしてあげればOK。

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

goroutineでシェルっぽくバッチ処理を書いてみた

小さく分割してきれいにつくりたい、けど効率も重視したい

シェルや関数型プログラミングのように、部品の組み合わせでプログラムを書きたい。しかし、パイプラインや遅延評価みたいな仕組みがないと、ループ回数だけ増えてしまって効率が悪い。仕方がないから、1つの処理の中に全機能まとめてしまおう。そんな経験はないでしょうか。

それgo言語でできる!

go言語のgoroutineとchannelを勉強してみてまさにこれだと思い、go言語の勉強を兼ねて作ってみました。

前提環境

  • goバージョン:1.14

完成イメージ

シェルっぽく使いたいということで、シェルを意識した設計にしています。メソッドは基本的にchannelに関連付けたメソッドとして定義し、戻り値にはchannelを返すようにしました。そうすることで、メソッドチェーンをシェルのパイプに見立てて使えます。ただし、起点となる処理はchannelにすると逆に使いにくいので、普通に値を引数をとるようにします。

slip.Cat("data/slip.csv").Filter().Sort()

また、2つ以上のchannelをインプットにする処理については、メソッドチェーンで受け取れないので、引数でchannelを渡します。これは、逆にシェルにはない利点かと思います。シェルだと合流地点で同期をとる必要がありますが、goなら戻り値のchannelをそのまま渡すだけなので、見た目的にも効率的にも理想的です。

slipitem.Match(
        // tr: cat -> filter -> sort
        slip.Cat("data/slip.csv").Filter().Sort(),
        // ma: cat -> sort
        item.Cat("data/item.csv").Sort()
)

フィルターやソートの条件は都度変えたいので、引数で渡せるようにします。goでは関数が第一級オブジェクトとして扱われるので、関数の引数に関数を指定することができます。

  slip.Cat("data/slip.csv").Filter(func(slip slip.Slip) bool {
            return slip.No >= "20000"
        }).Sort(func(slips slip.Slips, i, j int) bool {
            return slips[i].ItemCode < slips[j].ItemCode
        })
)

このようなイメージでよくあるバッチ処理を書いてみました。main処理の完成形が下記です。ファイルに保存されているTR(伝票)とMA(商品)を商品コードでマッチングするイメージです。goroutineとchannelはmain処理からは隠蔽されており、自然な見た目になっていると思います。

main.go
package main

import (
    "fmt"
    "go-batch-sample/item"
    "go-batch-sample/slip"
    "go-batch-sample/slipitem"
)

func main() {
    // Cat, Filter, Sort, Matching and file out sample
    slipitem.Match(
        // tr: cat -> filter -> sort
        slip.Cat("data/slip.csv").Filter(func(slip slip.Slip) bool {
            return slip.No >= "20000"
        }).Sort(func(slips slip.Slips, i, j int) bool {
            return slips[i].ItemCode < slips[j].ItemCode
        }),
        // ma: cat -> sort
        item.Cat("data/item.csv").Sort(func(items item.Items, i, j int) bool {
            return items[i].Code < items[j].Code
        })).Out("data/slipItem.csv")
}

各メソッドの実現

メイン処理をメソッドチェーンでコーディングしていけるようにするには、go func()をメイン処理で使わないようにします。そのためには、go func()をラップするメソッドが必要です。メソッドの基本構造は、ラッパーメソッドと内部の即時関数という構造にしました。基本的な処理の流れは、戻り値用のchannelを作り、即時関数をgoroutineで非同期実行して(その中でchannelに送信していき)、戻り値用のchannelをreturnする、という流れです。また、前メソッドの戻り値のchannelをメソッドチェーンで受け取るように、channelに対してメソッドを定義します。goでは構造体にメソッドを定義することはできるのですが、channelに対しては、メソッドを定義することができません。そこで、単にchannelをラップするため型のエイリアスを設けます。

type Slip struct {
    No       string
    ItemCode string
    Count    int
}

type Slipchan chan Slip

ファイル読み込み(Cat)

ファイルの読み込みは、起点になる処理なので、Inputとしてchannelは受け取らず、ファイル名を引数にとり、戻り値にchannelを返却します。即時関数をgo func()してgoroutineで実行し、処理完了を待たずにchannelを返します。即時関数の中では、ファイルを1行1行読み込み、そのたびに、channelに送信していきます。ファイルの中身をすべて読み込み終わったら、処理を終了します。関数の最初で、defer close(ch)しているので、関数の終了に伴ってchannelがクローズされ、次のメソッドにクローズが通知されます。

func Cat(filename string) Slipchan {
    ch := make(Slipchan)
    go func(filename string, ch Slipchan) {
        defer close(ch)

        // file open
        file, err := os.Open(filename)
        if err != nil {
            panic(err)
        }
        defer file.Close()

        // read while file end
        reader := csv.NewReader(file)
        //  reader.Comma = '\t'

        for {
            line, err := reader.Read()

            switch err {
            case nil:
                // format to slip
                slip := Slip{}
                slip.No = line[0]
                slip.ItemCode = line[1]
                slip.Count, _ = strconv.Atoi(line[2])

                // send to channel
                ch <- slip

            case io.EOF:
                return

            default:
                panic(err)
            }
        }
    }(filename, ch)
    return ch
}

フィルター(Filter)

フィルターはInputとして前のメソッドからchannelを受けとります。channelの受け取りはメソッドチェーンとして行うので、channelの別名Slipchan型に対するメソッドとして定義します。また、フィルタリング条件は、検証関数を引数として受け取る形とします。戻り値の考え方は同じです。

func (ich Slipchan) Filter(test func(slip Slip) bool) Slipchan {
    och := make(Slipchan)
    go func(ich, och chan Slip) {
        defer close(och)
        for slip := range ich {
            if test(slip) {
                och <- slip
            }
        }
    }(ich, och)
    return och
}

ソート(Sort)

ソートもフィルターと同様、channelをメソッドチェーンで受け取り、channelを戻り値で返します。ただ、ソート条件の関数を渡すのに苦戦しました。sort.SliceStableを利用すると、条件を実行時に渡す安定ソートが行えるのですが、ラッパーメソッドの引数に渡した関数をそのままsort.SliceStableの引数に渡そうとするとうまくいきませんでした。SliceStableでは、第1引数のソート対象のsliceを第2引数の比較関数の中で利用する形で関数を定義します。クロージャーのようなイメージです。そのため、ラッパーメソッドの引数をそのまま利用するのは難しく、苦肉の策として、内部の即時関数の中で呼び出すsort.SliceStableに渡すの比較関数の中の比較処理として呼び出してもらうような関数を渡すことにしました。(ソースを見たほうが早そうですね。)

type Slips []Slip

func (ich Slipchan) Sort(sortfn func(slips Slips, i, j int) bool) Slipchan {
    och := make(Slipchan)
    go func(ich, och chan Slip) {
        defer close(och)
        tmpSlips := Slips{}
        for slip := range ich {
            tmpSlips = append(tmpSlips, slip)
        }

        sort.SliceStable(tmpSlips, func(i, j int) bool {
            return sortfn(tmpSlips, i, j)
        })

        for _, slip := range tmpSlips {
            och <- slip
        }
    }(ich, och)
    return och
}

mainから利用するときはこうなります。

main.go
.Sort(func(slips slip.Slips, i, j int) bool {
            return slips[i].ItemCode < slips[j].ItemCode
        })

現状のソートは、一度メモリにすべてため込んでからソートをかけています。もう少し特性にあったソートに変えたいところです。

マッチング処理(Match)

マッチングは2つのchannelが必要なため、メソッドチェーンではなく、引数でchannelを渡します。channelは最初からすべてのデータがわかるわけではないので、Mapのようにkey指定で取得することはできません。また、channelは1週しかできないので、単純に二重ループしてマッチングすると、エラーになってしまします。ma/trをひとつづつchannelから受信していく必要があります。全trの終了の判定は、channelから受信できなくなった(closeされた)のを、チェックするようにしています。

func Match(trch slip.Slipchan, mach item.Itemchan) SlipItemchan {
    outch := make(SlipItemchan)

    go func(trch slip.Slipchan, mach item.Itemchan, outch SlipItemchan) {
        defer close(outch)

        tr := <-trch
        ma := <-mach

        for {
            switch {
            case tr.ItemCode == ma.Code:
                // match
                slipItem := SlipItem{}
                slipItem.Slip = tr
                slipItem.ItemName = ma.Name
                outch <- slipItem

                trtmp, ok := <-trch
                if ok {
                    // if tr exists, update tr
                    tr = trtmp
                } else {
                    // if tr ends, fisnish
                    return
                }

            default:
                matmp, ok := <-mach
                if ok {
                    // if master exists, update ma
                    ma = matmp
                } else {
                    // if master ends, error
                    panic("ma ends")
                }
            }
        }
    }(trch, mach, outch)

    return outch
}

ファイル出力(Out)

ファイル出力は、channelをメソッドチェーンで受け取って、channelから逐次ファイル書き込みしていきます。ファイル出力しきって処理完了としたいため、goroutine化せずに、同期処理として書いています。teeみたいにしたい場合は、その他のメソッドと同じように内部の即時関数を使ってgoroutineにしてもよいかもしれません。

func (ch SlipItemchan) Out(filename string) {
    file, err := os.Create(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    for slipItem := range ch {
        fmt.Fprintln(file, slipItem.No, ",", slipItem.ItemCode, ",", slipItem.ItemName, ",", slipItem.Count)
    }
}

go言語はイケてる

goroutineを隠蔽化してmainメソッドはかなりすっきり表現できたと思います。各データ毎に基本セットを用意しておけば、生産性高くメイン処理を書いていけるのではないでしょうか。それぞれのメソッドの実装も特に複雑なことはなく、単純なパターンに落とし込めているかと思います。ただ、いくらgoroutineがプロセスやスレッドに比べて軽量といっても、こんな感じでポコポコ起動してよいものなのか勘所がないので、その検証は必要そうです。また、効率を考えてはいるものの、本当にシェルのパイプラインのように高速に動いてくれるのかは検証できていません。時間があれば試してみたいです。

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