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

AtCoder Beginner Contest 192のメモ

前置き

Atcoderをやってみたので、自分用のメモです。
あとから加筆・修正する予定です。

問題

https://atcoder.jp/contests/abc192

A

Q_A.go
package main

import (
    "fmt"
)

func main() {
    var X int
    fmt.Scanf("%d", &X)

    amari := X % 100

    ans := 100 - amari

    fmt.Printf("%d\n", ans)
}

B

Q_B.go
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func IsFirstUpper(v string) bool {
    if v == "" {
       return false;
    }
    r := rune(v[0])
    return unicode.IsUpper(r)
} 

func main() {
    var S string

    fmt.Scanf("%s", &S)
    s := strings.Split(S, "")

    var ans bool = true
    for i:=0; i<len(s); i++{
        check := IsFirstUpper(s[i])
        if i % 2 == 0{
            if check{
                ans = false
            }
        } else {
            if !check{
                ans = false
            }
        }
    }

    if ans {
        fmt.Printf("Yes\n")
    } else {
        fmt.Printf("No\n")
    }
}

C

Q_C.go
package main

import (
    "fmt"
    "strconv"
    "strings"
    "sort"
)

func calc (a int) int{

    a_s := strconv.Itoa(a)

    a_s_1 := strings.Split(a_s, "")
    a_s_2 := strings.Split(a_s, "")

    sort.Strings(a_s_1)
    sort.Slice(a_s_2, func(i, j int) bool {
        return a_s_2[i] > a_s_2[j]
    })

    a_1 := strings.Join(a_s_1, "")
    a_2 := strings.Join(a_s_2, "")

    a__1, _ := strconv.Atoi(a_1)
    a__2, _ := strconv.Atoi(a_2)

    ans := a__2 - a__1
    return ans
}

func main() {
    var N, K int
    fmt.Scanf("%d %d", &N, &K)

    for i:=0; i<K ; i++{
        N = calc(N)
    }

    fmt.Printf("%d\n", N)   
}

D

覚えてたら後で書きます。

E

覚えてたら後で書きます。

F

覚えてたら後で書きます。

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

GitHub Actionsでスクレイピングを定期実行して結果をSlackに通知する

はじめに

GitHub Actionsで処理を自動化する話です。
Go言語でスクレイピングして結果をSlackに通知します。

参考にさせていただいた記事

準備

GitHubとSlackのアカウントが必要です。
Slackはフリープランでスペースを作成し、Botから通知を送るためのチャンネルを作成しておきます。GitHubにはプライベートのリポジトリを作成しておきます(DDoS攻撃などに悪用されないため非公開にする)

流れ

以下の順番で作成を進めます。

  1. 対象ページから必要な情報を取得する処理の作成
  2. Slackアプリの作成とトークンの取得
  3. SlackにPOSTする処理の作成
  4. GitHub Actionsで処理を定時実行できるようにする

対象ページから必要な情報を取得する処理の作成

Go言語でスクレイピングする際の定番パッケージである(ように思われる)PuerkitoBio/goqueryを使用します。スクレイピングするからには有用でなおかつ毎日更新される情報が欲しいので、Yahoo!ショッピングの日替わりクーポンを取得してみます。https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/index.html このURLがスクレイピングの対象です。

サンプル https://github.com/PuerkitoBio/goquery#examples を参考にしつつtitleタグの中身だけ取得する処理を作成します。

main.go
package main

import (
        "fmt"
        "log"
        "net/http"

        "github.com/PuerkitoBio/goquery"
)

func main() {
        // Request the HTML page.
        res, err := http.Get("https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/index.html")
        if err != nil {
                log.Fatal(err)
        }
        defer res.Body.Close()

        if res.StatusCode != 200 {
                log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
        }

        // Load the HTML document
        doc, err := goquery.NewDocumentFromReader(res.Body)
        if err != nil {
                log.Fatal(err)
        }

        title := doc.Find("title").Text()
        fmt.Println(title)
}

ビルドして実行するとタイトルの取得結果を確認することができます。

$ go mod init
$ go build
$ ./goquery_test
【今日のクーポン】メガネ、くもり止めカテゴリで使える30%OFFクーポン

Slackアプリの作成とトークンの取得

SlackにメッセージをPOSTする処理を作成する前にSlackアプリ(Bot)を作成し、トークンを取得しておく必要があります。

  1. https://api.slack.com/apps にアクセスしてCreate New Appボタンを押下
  2. App NameDevelopment Slack Workspaceを入力してCreate Appボタンを押下
    Screenshot 2021-02-12 at 20.54.29.png

  3. 左カラムのFeatures配下にあるOAuth & Permissionsをクリック
    Screenshot 2021-02-20 at 11.09.55.png

  4. Bot Token Scopeschat:writeを追加(Add an OAuth Scopeをクリックしてリストから選択)
    Screenshot 2021-02-20 at 10.27.31.png

  5. 左カラムのSettings配下にあるInstall Appをクリック

  6. Install to Workspaceボタンを押下
    Screenshot 2021-02-20 at 11.13.01.png

7.ワークスペースにアプリのインストールが完了するとトークン(OAuth Access Token)が表示されるのでメモしておく。
Screenshot 2021-02-20 at 11.15.56 (1).png

8.メッセージを受信したいチャンネルに作成したアプリを追加しておく。ショートカットを開いてinviteと入力、表示されたリストからこのチャンネルにアプリを追加するを選択する。
Screenshot 2021-02-20 at 13.38.01.png

SlackにPOSTする処理の作成

続いてSlackにPOSTする処理を作成します。Qiitaの多くの記事にも書かれているslack-go/slackパッケージを使います。examples以下を探したところ単純にメッセージを送信するサンプルを見つけることができました。https://github.com/slack-go/slack/tree/master/examples/messages こちらをベースに作成します。

2箇所書き換えが必要です。
YOUR_TOKEN_HERE : メモしておいたOAuth Access Tokenに書き換え
CHANNEL_ID : メッセージを受信したいチャンネル名かチャンネルIDを指定

main.go
package main

import (
    "fmt"

    "github.com/slack-go/slack"
)

func main() {
    // YOUR_TOKEN_HEREをメモしておいたトークンに置き換える
    api := slack.New("YOUR_TOKEN_HERE")
    attachment := slack.Attachment{
        Pretext: "some pretext",
        Text:    "some text",
        // Uncomment the following part to send a field too
        /*
            Fields: []slack.AttachmentField{
                slack.AttachmentField{
                    Title: "a",
                    Value: "no",
                },
            },
        */
    }

    channelID, timestamp, err := api.PostMessage(
        "CHANNEL_ID", // メッセージを送信したいチャンネルを指定する
        slack.MsgOptionText("Some text", false),
        slack.MsgOptionAttachments(attachment),
        slack.MsgOptionAsUser(true), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot
    )
    if err != nil {
        fmt.Printf("%s\n", err)
        return
    }
    fmt.Printf("Message successfully sent to channel %s at %s\n", channelID, timestamp)
}

ビルドして実行するとSlackにメッセージが送信されます。

$ go mod init
$ go build
$ ./slack_bot
Message successfully sent to channel XXXXXXXXXXXXX at 1613796836.000500

Screenshot 2021-02-20 at 13.55.29 (1).png
not_in_channelとエラーが出力された場合にはチャンネルにアプリを追加して下さい。

GitHub Actionsで処理を定時実行できるようにする

まずは動作確認用のプライベートリポジトリを作成しておく。Hello, Worldを出力するだけの処理を作りgo.modも含めてリポジトリに追加しておく。

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Actionsタブをクリックするとそのリポジトリに適したワークフローテンプレートがサジェストされます。サジェストされたGo言語用のテンプレートをベースにしつつワークフローを作成します。cronによる定期実行だけではなく手動で実行するためworkflow_dispatchを登録しておくと確認が楽になります。scheduleイベントはcron形式で記述できますがUTC時間なので注意が必要です。
詳しくはコチラ https://docs.github.com/ja/actions/reference/events-that-trigger-workflows

作成したワークフローファイルは、リポジトリの.github/workflowsディレクトリに登録します。ワークフローを手動実行するか設定した時刻になれば結果を確認できるはずです。ワークフローの手動実行方法 https://docs.github.com/ja/actions/managing-workflow-runs/manually-running-a-workflow

cron.yml
name: Go

on:
  workflow_dispatch:
  schedule:
    - cron: '5 15 * * *'

jobs:

  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.15

    - name: Build
      run: |
        mkdir /home/runner/work/dist
        go build -v -o /home/runner/work/dist/myapp ./...
    - name: RunMyApp
      run: /home/runner/work/dist/myapp

まとめ+α

仕上げとしてスクレイピング+Slack送信の処理を作成します。さらにトークンはシークレットから取得するようにしておきます。シークレットはリポジトリのSettingsタブのsecretsから登録する。下に掲載したサンプル(main.go)ではトークンをSLACK_TOKENにしています。

main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/PuerkitoBio/goquery"
    "github.com/slack-go/slack"
)

func mustGetenv(k string) string {
    v := os.Getenv(k)
    if v == "" {
        log.Panic("env not set.")
    }
    return v
}

var token string

func init() {
    token = mustGetenv("SLACK_TOKEN")
}

func main() {
    // Request the HTML page.
    res, err := http.Get("https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/index.html")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()

    if res.StatusCode != 200 {
        log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
    }

    // Load the HTML document
    doc, err := goquery.NewDocumentFromReader(res.Body)
    if err != nil {
        log.Fatal(err)
    }

    title := doc.Find("title").Text()

    api := slack.New(token)

    attachment := slack.Attachment{
        Text: "https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/",
    }

    channelID, timestamp, err := api.PostMessage(
        "CHANNEL_ID",
        slack.MsgOptionText(title, false),
        slack.MsgOptionAttachments(attachment),
        slack.MsgOptionAsUser(true),
    )
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Message successfully sent to channel %s at %s\n", channelID, timestamp)
}

ワークフローにもシークレットを取得してセットする処理を追加しておきます。(追加した前後だけ抜粋)

cron.yml
name: Go

env:
  SLACK_TOKEN: ${{secrets.SLACK_TOKEN}}

on:
  workflow_dispatch:
  schedule:
    - cron: '5 15 * * *'

処理や設定に問題がなければ以下のように毎日通知が届くようになります。
Screenshot 2021-02-20 at 22.41.39.png

最後に

GitHub Actionsで何か自動化してみたかったのでスクレイピングしてSlackに通知をしてみましたが、スクレイピングする場合にはルールとマナーを守りましょう。

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

Goのencoding/json.Decoderが構造体フィールドにjson:"-"タグがついていたら無視する処理を探してみた

コンテキスト

  • Goの構造体のフィールドには"tag_name:tag_value"というようにタグをつけることができる。
  • jsonのエンコードやデコードで使われるjsonタグというものがある。
  • json.Marshalで構造体->jsonのエンコードする場合に、構造体のフィールドのjsonタグに紐づいた文字列がjsonにおける文字列となる。
  • jsonタグに"-"という値を紐付けると、jsonには出力されないフィールドとなる。
type Struct struct {
    Name        string `json:"name"`
    Age         int
    Credential  string  `json:"-"`
}

上の構造体は以下のようなjsonに変換されてエンコードされる。

{"name": "Tomori Yu", "Age": 21}

疑問

  • エンコードに関する挙動は以上に見た通りだが、デコードの挙動はどうなっているのだろう。

試してみる

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

type person struct {
    Name       string `json:"name"`
    Age        int
    Credential string `json:"-"`
}

func main() {
    p1 := person{"yu", 21, "AKIi9854u4t8394j8gf"}
    bs1, _ := json.Marshal(p1)
    fmt.Printf("marshaled person: %v\n", string(bs1))
    // -> marshaled person: {"name":"yu","Age":21}

    var p2 person
    _ = json.Unmarshal(bs, &p2)
    fmt.Printf("unmarshaled person: %v\n", p2)
    // -> unmarshaled person: {yu 21 }

    bs2 := []byte(`{"name": "yu", "Age": 21, "Credential": "AKIi9854u4t8394j8gf"}`)
    decoder := json.NewDecoder(bytes.NewBuffer(bs2))
    var p3 person
    _ = decoder.Decode(&p3)
    fmt.Printf("decoded person: %v\n", p3)
    // -> decoded person: {yu 21 }
}
  • json.MarshalではAgeがアウトプットに残るが、json.Unmarshal&json.DecodeではAgeが残らない。
  • 構造体->jsonの場合と同じように、json->構造体のデコードにおいても構造体のフィールドにjson:"-"というタグが付いていれば無視される。

json.Decoderに関して構造体のフィールドにjson:"-"というタグが付いていたら無視する処理を探してみた

encoding/json.Decoder

decoder := json.NewDecoder(buffer)

json.NewDecoderはDecoder構造体を返す。

Decoder

type Decoder struct {
    r       io.Reader
    buf     []byte
    d       decodeState
    scanp   int   // start of unread data in buf
    scanned int64 // amount of data already scanned
    scan    scanner
    err     error

    tokenState int
    tokenStack []int
}

Decoder構造体のポインタ型に対するDecodeメソッドは引数として渡した変数にデコードした結果を返す。
Decoder.Decode

func (dec *Decoder) Decode(v interface{}) error {
    if dec.err != nil {
        return dec.err
    }

    if err := dec.tokenPrepareForDecode(); err != nil {
        return err
    }

    if !dec.tokenValueAllowed() {
        return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
    }

    // Read whole value into buffer.
    n, err := dec.readValue()
    if err != nil {
        return err
    }
    dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
    dec.scanp += n

    // Don't save err from unmarshal into dec.err:
    // the connection is still usable since we read a complete JSON
    // object from it before the error happened.
    err = dec.d.unmarshal(v)

    // fixup token streaming state
    dec.tokenValueEnd()

    return err
}

dec.dは、Decoder構造体のフィールドであり、decodeState構造体のインスタンスである。
dec.d.initでdecodeState.dataにまだ読まれてない[]byteを初期化している。
そして、err = dec.d.unmarshal(v)に、デコードの処理がありそうだ。
decodeState

type decodeState struct {
    data         []byte
    off          int // next read offset in data
    opcode       int // last read result
    scan         scanner
    errorContext struct { // provides context for type errors
        Struct     reflect.Type
        FieldStack []string
    }
    savedError            error
    useNumber             bool
    disallowUnknownFields bool
}

decodeState.unmarshal

func (d *decodeState) unmarshal(v interface{}) error {
    rv := reflect.ValueOf(v)
    if rv.Kind() != reflect.Ptr || rv.IsNil() {
        return &InvalidUnmarshalError{reflect.TypeOf(v)}
    }

    d.scan.reset()
    d.scanWhile(scanSkipSpace)
    // We decode rv not rv.Elem because the Unmarshaler interface
    // test must be applied at the top level of the value.
    err := d.value(rv)
    if err != nil {
        return d.addErrorContext(err)
    }
    return d.savedError
}

d.scanは、decodeState構造体のscanフィールドであり、scanner構造体のフィールドである。
scannerはjsonをパースする処理におけるステートマシンとして利用される。
d.scan.reset()でdecodeStateのフィールドであるscannerインスタンスが初期化される。
d.value(rv)でデコード処理が行われていそうだ。
scanner

// A scanner is a JSON scanning state machine.
// Callers call scan.reset and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in.  (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
    // The step is a func to be called to execute the next transition.
    // Also tried using an integer constant and a single func
    // with a switch, but using the func directly was 10% faster
    // on a 64-bit Mac Mini, and it's nicer to read.
    step func(*scanner, byte) int

    // Reached end of top-level value.
    endTop bool

    // Stack of what we're in the middle of - array values, object keys, object values.
    parseState []int

    // Error that happened, if any.
    err error

    // total bytes consumed, updated by decoder.Decode (and deliberately
    // not set to zero by scan.reset)
    bytes int64
}

decodeState.value

// value consumes a JSON value from d.data[d.off-1:], decoding into v, and
// reads the following byte ahead. If v is invalid, the value is discarded.
// The first byte of the value has been read already.
func (d *decodeState) value(v reflect.Value) error {
    switch d.opcode {
    default:
        panic(phasePanicMsg)

    case scanBeginArray:
        if v.IsValid() {
            if err := d.array(v); err != nil {
                return err
            }
        } else {
            d.skip()
        }
        d.scanNext()

    case scanBeginObject:
        if v.IsValid() {
            if err := d.object(v); err != nil {
                return err
            }
        } else {
            d.skip()
        }
        d.scanNext()

    case scanBeginLiteral:
        // All bytes inside literal return scanContinue op code.
        start := d.readIndex()
        d.rescanLiteral()

        if v.IsValid() {
            if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
                return err
            }
        }
    }
    return nil
}

v.IsValid()はvがゼロ値でなければtrueを返します。
構造体を引数に渡した場合は、d.object(v)が呼ばれるっぽいです。

decodeState.object()を読むと、switch文で引数のunderlyingな型がStructな時の処理が書かれてあります。

    case reflect.Struct:
        fields = cachedTypeFields(t)
        // ok

fieldsはstructFields構造体のインスタンスです。
structFields

type structFields struct {
    list      []field
    nameIndex map[string]int
}

cachedTypeFields関数は、typeFields関数にキャッシュ機能を足した薄いラッパーとなっています。
typeFieldsは以下のようになっています。
https://github.com/golang/go/blob/master/src/encoding/json/encode.go#L1209-L1391

// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func typeFields(t reflect.Type) structFields {
    // Anonymous fields to explore at the current level and the next.
    current := []field{}
    next := []field{{typ: t}}
[省略]
                tag := sf.Tag.Get("json")
                if tag == "-" {
                    continue
                }
[省略]

ありました。sfは構造体のフィールドです。
forの中でcontinueされなかったフィールドはforブロックの後の処理でfields = append(fields, field)という感じでフィールドとして登録されます。
構造体のフィールドのうちjsonタグに"-"が付いているものはcontinueされているので、無視することになっているみたいです。

先ほど見たdecodeState.object()ではfields = cachedTypeFields(t)という感じで構造体の有効なフィールドだけが処理されることになります。
その後にfields変数に関して、以下の処理が書かれています。
https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L696-L709

            if i, ok := fields.nameIndex[string(key)]; ok {
                // Found an exact name match.
                f = &fields.list[i]
            } else {
                // Fall back to the expensive case-insensitive
                // linear search.
                for i := range fields.list {
                    ff := &fields.list[i]
                    if ff.equalFold(ff.nameBytes, key) {
                        f = ff
                        break
                    }
                }
            }

jsonのkeyにマッチする有効なStructのフィールドが見つかればfという変数に入れられて後の処理に続くようです。

まとめ

  • json.Decodeもjson:"-"タグのついた構造体のフィールドは無視する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語とClean ArchitectureでAPIサーバを構築する

Clean Architectureとは

Clean Architecture(クリーンアーキテクチャ)とは,レイヤに内側と外側の関係性を持たせるアーキテクチャである.
「外側のレイヤは内側のレイヤだけに依存する」というルールを守ることによって,アプリケーションから技術を分離することが目的である.

アプリケーションから技術を分離すると何が嬉しいのか

ここでの「技術」とは,HTTPやcsv,MySQLなどのことを意味している.
アプリケーションから技術を分離すると,技術を容易に変更できたり,テストコードを書くときに容易にモックできたりする.
例えば,出力をHTTPからcsvに変更したくなったときなどに容易に変更が可能である.

各レイヤの責務

Clean Architectureで提案されているレイヤ構造は以下の画像のようなものである.
CleanArchitecture.jpg
内側から,Entitiesレイヤ,Use Casesレイヤ,Interface Adaptersレイヤ,Frameworkd & Driversレイヤの4つのレイヤから構成される.
「外側のレイヤは内側のレイヤだけに依存する」というルールが存在し,例えば,Use CasesレイヤがExternal Interfacesレイヤに依存するようなことがあってはならない.
また,技術に依存しているコードを置いていいのはInterface Adaptersレイヤ,Frameworkd & Driversレイヤの外側2層だけで,Entitiesレイヤ,Use Casesレイヤには技術に依存したコードをおいてはならない.

各レイヤの責務を大まかに説明すると次のようなものである.

Entitiesレイヤ

ドメインロジックを実装する責務を持つ.
DB操作などの技術的な実装を持ってはならない.
また,他のどのレイヤにも依存してはならない.

Use Casesレイヤ

Entitiesレイヤのオブジェクトを操作してビジネスロジックを実行する責務を持つ.
さらに,このレイヤにはポートを定義する.ここで,ポートとは,アダプターで実装を差し替えることができる対象のことである.
Go言語の場合,ポートはInterfaceにあたる.
InputPort・OutputPortはそれぞれ,入力・出力に関するポートである.

Interface Adaptersレイヤ

Use Casesレイヤで定義したポートに対する実装を提供する.すなわち,InterfaceのMethodを定義する(実態を作ると考えるとよい).
それゆえ,このレイヤでDB操作やHTTP入出力などの技術的な実装を定義する.
Controllersは入力に関するアダプター,Presentersは出力に関するアダプター,Gatewaysは永続化に関するアダプターである.

Frameworks & Driversレイヤ

DBのconnection生成やroutingなどの技術的な実装をおく.

Golangを用いてAPIサーバを構築する.

ここで作成するAPIはPathParameterからuserIDを受け取り,そのuserIDをもつuserの名前をDBから取得し,出力するものである.

GET /user/:id
input: userID string
output: userName string

以下で出てくるコードは全て サンプルコード においてある.

package構成

大まかなpackage構成が以下である.

.
├── adapter
│   ├── controller
│   │   └── user.go
│   ├── gateway
│   │   └── user.go
│   └── presenter
│       └── user.go
├── driver
│   └── user.go
├── entity
│   └── user.go
└── usecase
    ├── interactor
    │   └── user.go
    └── port
        └── user.go

package同士の関係

package同士の関係は以下の画像のとおりである.ただし,重要な部分だけを抜き出している.
実線は依存(使用していると読み替えても良い),点線は実装(Interfaceを満たすようにMethodを定義)を表している.
architecture.png

大まかな流れとしては,以下のようなものである.

  1. driverからadapter/controllerを呼び出す
  2. adapter/controllerは,ポートを全て組み立てて,usecase/port/inputPortを実行する
  3. usecase/port/inputPortusecase/interactorが実装しているので,usecase/interactorのMethodが呼ばれる
  4. usecase/interactorでは,entityのドメインロジックを実行する.
  5. usecase/interactorでは,usecase/port/userRepositoryを呼び出し,DBの永続化処理を行う(usecase/port/userRepositoryadapter/gatewayが実装しているので,adapter/gatewayのMethodが呼ばれる)
  6. usecase/interactorでは,usecase/port/outputPortを呼び出し,出力を行う(usecase/port/outputPortadapter/presenterが実装しているので,adapter/presenterのMethodが呼ばれる)

通常のMVCなどでは,controllerが入力を受け取り,modelを呼び出しドメインロジックを実行し,controllerが出力を行うが,Clean Architectureでは,入力はinputPort,出力はoutputPortが担当していることに注意する.それゆえ,adapter/controllerでは,単にusecase/port/inputPortを実行するだけで,返り値を受け取ったり出力を行ったりする必要はない.

サンプルコード

次にサンプルコードを読みながらClean Architectureの流れを理解する.

1. driverからadapter/controllerを呼び出す

driver/user.go
package driver

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/ari1021/clean-architecture/adapter/controller"
    "github.com/ari1021/clean-architecture/adapter/gateway"
    "github.com/ari1021/clean-architecture/adapter/presenter"
    "github.com/ari1021/clean-architecture/usecase/interactor"

    // blank import for MySQL driver
    _ "github.com/go-sql-driver/mysql"
)

// Serve はserverを起動させます.
func Serve(addr string) {
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DATABASE"))
    conn, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Println(err)
        return
    }
    user := controller.User{
        OutputFactory: presenter.NewUserOutputPort,
        InputFactory:  interactor.NewUserInputPort,
        RepoFactory:   gateway.NewUserRepository,
        Conn:          conn,
    }
    http.HandleFunc("/user/", user.GetUserByID)
    err = http.ListenAndServe(addr, nil)
    if err != nil {
        log.Fatalf("Listen and serve failed. %+v", err)
    }
}

driver/user.goではDBのconnectionを生成し,routingの設定を行なっている.
adapter/controller/user.goで定義されているcontroller.Userを作成し,http.HandleFunc()controller.User.GetUserByIDを渡している


2. adapter/controllerは,ポートを全て組み立てて,usecase/port/inputPortを実行する

adapter/controller/user.go
package controller

import (
    "database/sql"
    "net/http"
    "strings"

    "github.com/ari1021/clean-architecture/usecase/port"
)

type User struct {
    OutputFactory func(w http.ResponseWriter) port.UserOutputPort
    // -> presenter.NewUserOutputPort
    InputFactory func(o port.UserOutputPort, u port.UserRepository) port.UserInputPort
    // -> interactor.NewUserInputPort
    RepoFactory func(c *sql.DB) port.UserRepository
    // -> gateway.NewUserRepository
    Conn *sql.DB
}

// GetUserByID は,httpを受け取り,portを組み立てて,inputPort.GetUserByIDを呼び出します.
func (u *User) GetUserByID(w http.ResponseWriter, r *http.Request) {
    userID := strings.TrimPrefix(r.URL.Path, "/user/")

    outputPort := u.OutputFactory(w)
    repository := u.RepoFactory(u.Conn)
    inputPort := u.InputFactory(outputPort, repository)
    inputPort.GetUserByID(userID)
}

1で呼ばれたcontroller.User.GetUserByID内では,入力を受け取りPathParmeterを取得した後,全てのポート(UserInputPort, UserOutputPort, UserRepository)を組み立てて,inputPort.GetUserByIDを呼び出す.


3. usecase/port/inputPortusecase/interactorが実装しているので,usecase/interactorのMethodが呼ばれる

usecase/port/user.go
package port

import "github.com/ari1021/clean-architecture/entity"

type UserInputPort interface {
    GetUserByID(userID string)
}

type UserOutputPort interface {
    Render(*entity.User)
    RenderError(error)
}

// userのCRUDに対するDB用のポート
type UserRepository interface {
    GetUserByID(userID string) (*entity.User, error)
}

usecase/port/user.goに定義されているUserInputPortはInterfaceなので,このInterfaceを実装しているコードが呼ばれることになる.
UserInputPortは,usecase/interactor/user.goで実装されている.

usecase/interactor/user.go
package interactor

import (
    "github.com/ari1021/clean-architecture/usecase/port"
)

type User struct {
    OutputPort port.UserOutputPort
    UserRepo   port.UserRepository
}

// NewUserInputPort はUserInputPortを取得します.
func NewUserInputPort(outputPort port.UserOutputPort, userRepository port.UserRepository) port.UserInputPort {
    return &User{
        OutputPort: outputPort,
        UserRepo:   userRepository,
    }
}

// usecase.UserInputPortを実装している
// GetUserByID は,UserRepo.GetUserByIDを呼び出し,その結果をOutputPort.Render or OutputPort.RenderErrorに渡します.
func (u *User) GetUserByID(userID string) {
    user, err := u.UserRepo.GetUserByID(userID)
    if err != nil {
        u.OutputPort.RenderError(err)
        return
    }
    u.OutputPort.Render(user)
}

usecase/interactor/user.goに定義されているInputPortの実装(GetUserByID)内では,まずport.UserRepository.GetUserByIDが呼ばれている(5).
その後,errの有無により,port.UserOutputPort.Renderport.UserOutputPort.RenderErrorが呼ばれている(6).


4. usecase/interactorでは,entityのドメインロジックを実行する.
今回は,単純にDBから取得したデータを出力しているのでこの部分の実装はない.


5. usecase/interactorでは,usecase/port/userRepositoryを呼び出し,DBの永続化処理を行う(usecase/port/userRepositoryadapter/gatewayが実装しているので,adapter/gatewayのMethodが呼ばれる)

adapter/gateway/user.go
package gateway

import (
    "database/sql"
    "errors"
    "fmt"
    "log"

    "github.com/ari1021/clean-architecture/entity"
    "github.com/ari1021/clean-architecture/usecase/port"
)

type UserRepository struct {
    conn *sql.DB
}

// NewUserRepository はUserRepositoryを返します.
func NewUserRepository(conn *sql.DB) port.UserRepository {
    return &UserRepository{
        conn: conn,
    }
}

// GetUserByID はDBからデータを取得します.
func (u *UserRepository) GetUserByID(userID string) (*entity.User, error) {
    conn := u.GetDBConn()
    row := conn.QueryRow("SELECT * FROM `user` WHERE id=?", userID)
    user := entity.User{}
    err := row.Scan(&user.ID, &user.Name)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("User Not Found. UserID = %s", userID)
        }
        log.Println(err)
        return nil, errors.New("Internal Server Error. adapter/gateway/GetUserByID")
    }
    return &user, nil
}

// GetDBConn はconnectionを取得します.
func (u *UserRepository) GetDBConn() *sql.DB {
    return u.conn
}

3のusecase/interactor/user.goで呼び出されたUserRepository.GetUserByIDadapter/gateway/user.goで実装されている.
adapter/gateway/user.goでは,DB操作を実装している.


6. usecase/interactorでは,usecase/port/outputPortを呼び出し,出力を行う(usecase/port/outputPortadapter/presenterが実装しているので,adapter/presenterのMethodが呼ばれる)

adapter/presenter/user.go
package presenter

import (
    "fmt"
    "net/http"

    "github.com/ari1021/clean-architecture/entity"
    "github.com/ari1021/clean-architecture/usecase/port"
)

type User struct {
    w http.ResponseWriter
}

// NewUserOutputPort はUserOutputPortを取得します.
func NewUserOutputPort(w http.ResponseWriter) port.UserOutputPort {
    return &User{
        w: w,
    }
}

// usecase.UserOutputPortを実装している
// Render はNameを出力します.
func (u *User) Render(user *entity.User) {
    u.w.WriteHeader(http.StatusOK)
    // httpでentity.User.Nameを出力
    fmt.Fprint(u.w, user.Name)
}

// RenderError はErrorを出力します.
func (u *User) RenderError(err error) {
    u.w.WriteHeader(http.StatusInternalServerError)
    fmt.Fprint(u.w, err)
}

3のusecase/interactor/user.goで呼び出されたUserOutputPort.Render(RenderError)はadapter/presenter/user.goで実装されている.
adapter/presenter/user.goでは,Headerを付与して出力を行っている.

アダプターの差し替え

上のサンプルコードのように,技術的な実装は全てInterface AdaptersレイヤとFrameworkd & Driversレイヤで行っている.それよりも内側の層は,技術が何を使われているかを知ることがない.
したがって,HTTP出力ではなく,ファイル出力に変えたければ,adapter/presenter/user.goRenderRenderErrorを変えるだけで良い.

まとめ

Clean Architectureでは,アプリケーションから技術を分離することが重要である.
その観点では,Hexagonal Architecture(ヘキサゴナルアーキテクチャ)でもClean Architectureと同様に,アプリケーションから技術を分離することができる.
それらの違いはレイヤ構造の細分化の程度であり,Clean Architectureの方がHexagonal Architectureよりも細分化されている.
ただし,Hexagonal Architectureを実際に用いることを考えると,レイヤをさらに細かく分割すると思われるので,結局,Clean Architectureに類似していくと考えられる.

また,ここで紹介したパッケージ構成はあくまでも一例である.
例えば,今回はUserRepositoryusecase/port内に配置したが,UserRepositoryentityに置くという選択肢などもある.
(Clean Architecture で実装するときに知っておきたかったこと)

参考文献

この記事は以下の情報を参考にして執筆しました.
pospomeのサーバサイドアーキテクチャ(PDF版)
Clean Architecture で実装するときに知っておきたかったこと
The Clean Architecture

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

Go言語でのClean Architectureを理解する

Clean Architectureとは

Clean Architecture(クリーンアーキテクチャ)とは,レイヤに内側と外側の関係性を持たせるアーキテクチャである.
「外側のレイヤは内側のレイヤだけに依存する」というルールを守ることによって,アプリケーションから技術を分離することが目的である.

アプリケーションから技術を分離すると何が嬉しいのか

ここでの「技術」とは,HTTPやcsv,MySQLなどのことを意味している.
アプリケーションから技術を分離すると,技術を容易に変更できたり,テストコードを書くときに容易にモックできたりする.
例えば,出力をHTTPからcsvに変更したくなったときなどに容易に変更が可能である.

各レイヤの責務

Clean Architectureで提案されているレイヤ構造は以下の画像のようなものである.
CleanArchitecture.jpg
内側から,Entitiesレイヤ,Use Casesレイヤ,Interface Adaptersレイヤ,Frameworkd & Driversレイヤの4つのレイヤから構成される.
「外側のレイヤは内側のレイヤだけに依存する」というルールが存在し,例えば,Use CasesレイヤがExternal Interfacesレイヤに依存するようなことがあってはならない.
また,技術に依存しているコードを置いていいのはInterface Adaptersレイヤ,Frameworkd & Driversレイヤの外側2層だけで,Entitiesレイヤ,Use Casesレイヤには技術に依存したコードをおいてはならない.

各レイヤの責務を大まかに説明すると次のようなものである.

Entitiesレイヤ

ドメインロジックを実装する責務を持つ.
DB操作などの技術的な実装を持ってはならない.
また,他のどのレイヤにも依存してはならない.

Use Casesレイヤ

Entitiesレイヤのオブジェクトを操作してビジネスロジックを実行する責務を持つ.
さらに,このレイヤにはポートを定義する.ここで,ポートとは,アダプターで実装を差し替えることができる対象のことである.
Golangの場合,ポートはInterfaceにあたる.
InputPort・OutputPortはそれぞれ,入力・出力に関するポートである.

Interface Adaptersレイヤ

Use Casesレイヤで定義したポートに対する実装を提供する.すなわち,InterfaceのMethodを定義する(実態を作ると考えるとよい).
それゆえ,このレイヤでDB操作やHTTP入出力などの技術的な実装を定義する.
Controllersは入力に関するアダプター,Presentersは出力に関するアダプター,Gatewaysは永続化に関するアダプターである.

Frameworks & Driversレイヤ

DBのconnection生成やroutingなどの技術的な実装をおく.

Golangを用いてAPIサーバを構築する.

ここで作成するAPIはPathParameterからuserIDを受け取り,そのuserIDをもつuserの名前をDBから取得し,出力するものである.

GET /user/:id
input: userID string
output: userName string

以下で出てくるコードは全て サンプルコード においてある.

package構成

大まかなpackage構成が以下である.

.
├── adapter
│   ├── controller
│   │   └── user.go
│   ├── gateway
│   │   └── user.go
│   └── presenter
│       └── user.go
├── driver
│   └── user.go
├── entity
│   └── user.go
└── usecase
    ├── interactor
    │   └── user.go
    └── port
        └── user.go

package同士の関係

package同士の関係は以下の画像のとおりである.ただし,重要な部分だけを抜き出している.
実線は依存(使用していると読み替えても良い),点線は実装(Interfaceを満たすようにMethodを定義)を表している.
architecture.png

大まかな流れとしては,以下のようなものである.

  1. driverからadapter/controllerを呼び出す
  2. adapter/controllerは,ポートを全て組み立てて,usecase/port/inputPortを実行する
  3. usecase/port/inputPortusecase/interactorが実装しているので,usecase/interactorのMethodが呼ばれる
  4. usecase/interactorでは,entityのドメインロジックを実行する.
  5. usecase/interactorでは,usecase/port/userRepositoryを呼び出し,DBの永続化処理を行う(usecase/port/userRepositoryadapter/gatewayが実装しているので,adapter/gatewayのMethodが呼ばれる)
  6. usecase/interactorでは,usecase/port/outputPortを呼び出し,出力を行う(usecase/port/outputPortadapter/presenterが実装しているので,adapter/presenterのMethodが呼ばれる)

通常のMVCなどでは,controllerが入力を受け取り,modelを呼び出しドメインロジックを実行し,controllerが出力を行うが,Clean Architectureでは,入力はinputPort,出力はoutputPortが担当していることに注意する.それゆえ,adapter/controllerでは,単にusecase/port/inputPortを実行するだけで,返り値を受け取ったり出力を行ったりする必要はない.

サンプルコード

次にサンプルコードを読みながらClean Architectureの流れを理解する.

1. driverからadapter/controllerを呼び出す

driver/user.go
package driver

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/ari1021/clean-architecture/adapter/controller"
    "github.com/ari1021/clean-architecture/adapter/gateway"
    "github.com/ari1021/clean-architecture/adapter/presenter"
    "github.com/ari1021/clean-architecture/usecase/interactor"

    // blank import for MySQL driver
    _ "github.com/go-sql-driver/mysql"
)

// Serve はserverを起動させます.
func Serve(addr string) {
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DATABASE"))
    conn, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Println(err)
        return
    }
    user := controller.User{
        OutputFactory: presenter.NewUserOutputPort,
        InputFactory:  interactor.NewUserInputPort,
        RepoFactory:   gateway.NewUserRepository,
        Conn:          conn,
    }
    http.HandleFunc("/user/", user.GetUserByID)
    err = http.ListenAndServe(addr, nil)
    if err != nil {
        log.Fatalf("Listen and serve failed. %+v", err)
    }
}

driver/user.goではDBのconnectionを生成し,routingの設定を行なっている.
adapter/controller/user.goで定義されているcontroller.Userを作成し,http.HandleFunc()controller.User.GetUserByIDを渡している


2. adapter/controllerは,ポートを全て組み立てて,usecase/port/inputPortを実行する

adapter/controller/user.go
package controller

import (
    "database/sql"
    "net/http"
    "strings"

    "github.com/ari1021/clean-architecture/usecase/port"
)

type User struct {
    OutputFactory func(w http.ResponseWriter) port.UserOutputPort
    // -> presenter.NewUserOutputPort
    InputFactory func(o port.UserOutputPort, u port.UserRepository) port.UserInputPort
    // -> interactor.NewUserInputPort
    RepoFactory func(c *sql.DB) port.UserRepository
    // -> gateway.NewUserRepository
    Conn *sql.DB
}

// GetUserByID は,httpを受け取り,portを組み立てて,inputPort.GetUserByIDを呼び出します.
func (u *User) GetUserByID(w http.ResponseWriter, r *http.Request) {
    userID := strings.TrimPrefix(r.URL.Path, "/user/")

    outputPort := u.OutputFactory(w)
    repository := u.RepoFactory(u.Conn)
    inputPort := u.InputFactory(outputPort, repository)
    inputPort.GetUserByID(userID)
}

1で呼ばれたcontroller.User.GetUserByID内では,入力を受け取りPathParmeterを取得した後,全てのポート(UserInputPort, UserOutputPort, UserRepository)を組み立てて,inputPort.GetUserByIDを呼び出す.


3. usecase/port/inputPortusecase/interactorが実装しているので,usecase/interactorのMethodが呼ばれる

usecase/port/user.go
package port

import "github.com/ari1021/clean-architecture/entity"

type UserInputPort interface {
    GetUserByID(userID string)
}

type UserOutputPort interface {
    Render(*entity.User)
    RenderError(error)
}

// userのCRUDに対するDB用のポート
type UserRepository interface {
    GetUserByID(userID string) (*entity.User, error)
}

usecase/port/user.goに定義されているUserInputPortはInterfaceなので,このInterfaceを実装しているコードが呼ばれることになる.
UserInputPortは,usecase/interactor/user.goで実装されている.

usecase/interactor/user.go
package interactor

import (
    "github.com/ari1021/clean-architecture/usecase/port"
)

type User struct {
    OutputPort port.UserOutputPort
    UserRepo   port.UserRepository
}

// NewUserInputPort はUserInputPortを取得します.
func NewUserInputPort(outputPort port.UserOutputPort, userRepository port.UserRepository) port.UserInputPort {
    return &User{
        OutputPort: outputPort,
        UserRepo:   userRepository,
    }
}

// usecase.UserInputPortを実装している
// GetUserByID は,UserRepo.GetUserByIDを呼び出し,その結果をOutputPort.Render or OutputPort.RenderErrorに渡します.
func (u *User) GetUserByID(userID string) {
    user, err := u.UserRepo.GetUserByID(userID)
    if err != nil {
        u.OutputPort.RenderError(err)
        return
    }
    u.OutputPort.Render(user)
}

usecase/interactor/user.goに定義されているInputPortの実装(GetUserByID)内では,まずport.UserRepository.GetUserByIDが呼ばれている(5).
その後,errの有無により,port.UserOutputPort.Renderport.UserOutputPort.RenderErrorが呼ばれている(6).


4. usecase/interactorでは,entityのドメインロジックを実行する.
今回は,単純にDBから取得したデータを出力しているのでこの部分の実装はない.


5. usecase/interactorでは,usecase/port/userRepositoryを呼び出し,DBの永続化処理を行う(usecase/port/userRepositoryadapter/gatewayが実装しているので,adapter/gatewayのMethodが呼ばれる)

adapter/gateway/user.go
package gateway

import (
    "database/sql"
    "errors"
    "fmt"
    "log"

    "github.com/ari1021/clean-architecture/entity"
    "github.com/ari1021/clean-architecture/usecase/port"
)

type UserRepository struct {
    conn *sql.DB
}

// NewUserRepository はUserRepositoryを返します.
func NewUserRepository(conn *sql.DB) port.UserRepository {
    return &UserRepository{
        conn: conn,
    }
}

// GetUserByID はDBからデータを取得します.
func (u *UserRepository) GetUserByID(userID string) (*entity.User, error) {
    conn := u.GetDBConn()
    row := conn.QueryRow("SELECT * FROM `user` WHERE id=?", userID)
    user := entity.User{}
    err := row.Scan(&user.ID, &user.Name)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("User Not Found. UserID = %s", userID)
        }
        log.Println(err)
        return nil, errors.New("Internal Server Error. adapter/gateway/GetUserByID")
    }
    return &user, nil
}

// GetDBConn はconnectionを取得します.
func (u *UserRepository) GetDBConn() *sql.DB {
    return u.conn
}

3のusecase/interactor/user.goで呼び出されたUserRepository.GetUserByIDadapter/gateway/user.goで実装されている.
adapter/gateway/user.goでは,DB操作を実装している.


6. usecase/interactorでは,usecase/port/outputPortを呼び出し,出力を行う(usecase/port/outputPortadapter/presenterが実装しているので,adapter/presenterのMethodが呼ばれる)

adapter/presenter/user.go
package presenter

import (
    "fmt"
    "net/http"

    "github.com/ari1021/clean-architecture/entity"
    "github.com/ari1021/clean-architecture/usecase/port"
)

type User struct {
    w http.ResponseWriter
}

// NewUserOutputPort はUserOutputPortを取得します.
func NewUserOutputPort(w http.ResponseWriter) port.UserOutputPort {
    return &User{
        w: w,
    }
}

// usecase.UserOutputPortを実装している
// Render はNameを出力します.
func (u *User) Render(user *entity.User) {
    u.w.WriteHeader(http.StatusOK)
    // httpでentity.User.Nameを出力
    fmt.Fprint(u.w, user.Name)
}

// RenderError はErrorを出力します.
func (u *User) RenderError(err error) {
    u.w.WriteHeader(http.StatusInternalServerError)
    fmt.Fprint(u.w, err)
}

3のusecase/interactor/user.goで呼び出されたUserOutputPort.Render(RenderError)はadapter/presenter/user.goで実装されている.
adapter/presenter/user.goでは,Headerを付与して出力を行っている.

アダプターの差し替え

上のサンプルコードのように,技術的な実装は全てInterface AdaptersレイヤとFrameworkd & Driversレイヤで行っている.それよりも内側の層は,技術が何を使われているかを知ることがない.
したがって,HTTP出力ではなく,ファイル出力に変えたければ,adapter/presenter/user.goRenderRenderErrorを変えるだけで良い.

まとめ

Clean Architectureでは,アプリケーションから技術を分離することが重要である.
その観点では,Hexagonal Architecture(ヘキサゴナルアーキテクチャ)でもClean Architectureと同様に,アプリケーションから技術を分離することができる.
それらの違いはレイヤ構造の細分化の程度であり,Clean Architectureの方がHexagonal Architectureよりも細分化されている.
ただし,Hexagonal Architectureを実際に用いることを考えると,レイヤをさらに細かく分割すると思われるので,結局,Clean Architectureに類似していくと考えられる.

また,ここで紹介したパッケージ構成はあくまでも一例である.
例えば,今回はUserRepositoryusecase/port内に配置したが,UserRepositoryentityに置くという選択肢などもある.
(Clean Architecture で実装するときに知っておきたかったこと)

参考文献

この記事は以下の情報を参考にして執筆しました.
pospomeのサーバサイドアーキテクチャ(PDF版)
Clean Architecture で実装するときに知っておきたかったこと
The Clean Architecture

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

[Golang] 文字列を固定幅(固定長)で改行する。全角文字列の WordWrap

UTF-8 マルチバイト文字列を全角 n 文字(半角 n*2 文字)の固定長で改行させたい

対象文字列
あいうえおかきくけこさしすせそたちつてと
123456789012345678901234567890

n=6で改行
あいうえおか
きくけこさし
すせそたちつ
てと
123456789012
345678901234
567890

等幅フォントでない限り、全角 1 文字 == 半角 2 文字 の幅にならないのは当然として、それでも CLI アプリやコマンドのヘルプ表示など、なるべく等幅(1 行が固定長)になるように折り返したいのです。

しかし「"golang" 文字列 固定幅 改行」でググっても、なかなかヒットしなかったり、ルーンをゴニョゴニョしろとは言うもの具体的なものがなかったので、自分のググラビリティとして。

TL; DR (今北産業)

  1. @mattn さんの github.com/mattn/go-runewidth パッケージ(runewidth)を使う。 [ awesome-go パッケージ ]
  2. runewidth.Wrap(s string, w int) string メソッドで文字列 sw 文字幅で改行した文字列が取得できる。
  3. 全角 3 文字の場合は result := runewidth.Wrap(myString, 3 * 2) と 2 倍にする。

TS; DR

sample.go
package main

import (
    "fmt"
    "strings"

    "github.com/mattn/go-runewidth"
)

// WrapZenkakuString は文字列を固定幅で折り返します。幅の指定(width)は全角文字数を指定します。
// 半角文字は、その倍の文字数で折り返されます。主にヘルプ表示などに使われ、フォントによっては必ずし
// も固定幅になるとは限りません。
func WrapZenkakuString(input string, width int) string {
    var strTmp string

    strTmp = input
    strTmp = strings.Replace(strTmp, "\n", "", -1)
    strTmp = strings.Replace(strTmp, "\r", "", -1) // Windows
    strTmp = strings.Replace(strTmp, "。", "。\n", -1)

    return runewidth.Wrap(strTmp, width*2)
}

func main() {
    var msgHelp string = `
これはサンプルのヘルプです。
アイウエオかきくけこサシスセソたちつ
てとナニヌネコ。`

    fmt.Println(WrapZenkakuString(msgHelp, 6))
}

実行結果
$ go run .
これはサンプ
ルのヘルプで
す。
アイウエオか
きくけこサシ
スセソたちつ
てとナニヌネ
コ。

参考文献

関連文献

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

【Golang】文字列を固定幅(固定長)で改行する。全角文字列の WordWrap

UTF-8 マルチバイト文字列を全角 n 文字(半角 n*2 文字)の固定長で改行させたい

対象文字列
あいうえおかきくけこさしすせそたちつてと
123456789012345678901234567890

n=6で改行
あいうえおか
きくけこさし
すせそたちつ
てと
123456789012
345678901234
567890

等幅フォントでない限り、全角 1 文字 == 半角 2 文字 の文字幅にならないのは当然として、それでも CLI アプリやコマンドのヘルプ表示など、なるべく等幅(1 行が固定長)になるように折り返したいのです。

しかし「"golang" 文字列 固定幅 改行」でググっても、なかなかヒットしなかったり、ルーンをゴニョゴニョしろとは言うもの具体的なものがなかったので、自分のググラビリティとして。

TL; DR (今北産業)

  1. github.com/mattn/go-runewidth パッケージの runewidth を使う。 go-runewidth@mattn さんによる awesome-go なパッケージです。
  2. runewidth.Wrap(s string, w int) string メソッドで文字列 sw 文字幅で改行した文字列が取得できる。
  3. 全角 3 文字の場合は result := runewidth.Wrap(myString, 3 * 2) と 2 倍にする。

TS; DR

sample.go
package main

import (
    "fmt"
    "strings"

    "github.com/mattn/go-runewidth"
)

// WrapZenkakuString は文字列を固定幅で折り返します。幅の指定(width)は全角文字数を指定します。
// 半角文字は、その倍の文字数で折り返されます。主にヘルプ表示などに使われ、フォントによっては必ずし
// も固定幅になるとは限りません。
func WrapZenkakuString(input string, width int) string {
    var strTmp string

    strTmp = input
    strTmp = strings.Replace(strTmp, "\n", "", -1)
    strTmp = strings.Replace(strTmp, "\r", "", -1) // Windows
    strTmp = strings.Replace(strTmp, "。", "。\n", -1)

    return runewidth.Wrap(strTmp, width*2)
}

func main() {
    var msgHelp string = `
これはサンプルのヘルプです。
アイウエオかきくけこサシスセソたちつ
てとナニヌネコ。`

    fmt.Println(WrapZenkakuString(msgHelp, 6))
}

実行結果
$ go run .
これはサンプ
ルのヘルプで
す。
アイウエオか
きくけこサシ
スセソたちつ
てとナニヌネ
コ。

参考文献

関連文献

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