SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

storybook チュートリアル & CDD(コンポーネント駆動開発)

storybook チュートリアル & CDD(コンポーネント駆動開発)

チュートリアル & storybook 導入

storybook とは、UI コンポーネントとページを分離して開発を進めることができるツール。

npx degit chromaui/intro-storybook-react-template taskbox
cd taskbox
yarn
# test
yarn test --watchAll

# run storybook
yarn storybook

# run frontend app
yarn start

GUI test:

https://storybook.js.org/docs/react/writing-tests/test-runner

yarn add --dev @storybook/test-runner jest@27

npm run storybook

yarn test-storybook

ソース:

https://github.com/Symthy/react-practice/tree/main/react-storybook-tutorial

accessibility tests

アクセシビリティ・テストとは、WCAG ルールや業界で受け入れられているベスト・プラクティスに基づいた一連の経験則に対して、自動化されたツールを使用してレンダリングされた DOM を監査する方法。

これらは、視覚障害聴覚障害認知障害などの障害を持つユーザーを含む、できるだけ多くのユーザーがアプリケーションを使用できるようにする、露骨なアクセシビリティ違反をキャッチするための QA の最初のラインとして機能する。

Storybook には公式のアクセシビリティアドオンが含まれている。Deque の axe-core を使用しており、WCAG の問題の 57%を処理可能。

yarn add --dev @storybook/addon-a11y

main.js に追加

addons: [
    :
    '@storybook/addon-a11y',
  ]

CDD(コンポーネント駆動開発)

Component Driven User Interfaces

モジュール化されたコンポーネントを使用してユーザー・インタフェースを構築するための開発および設計プラクティス。UI は 「ボトムアップ」 から構築され、基本コンポーネントから始まり、段階的に結合されてスクリーンを組み立てる。

現代のユーザインターフェースは複雑。大規模に UI はもろい、デバッグ困難、出荷にじかんがかかるため、モジュール方式に分解することで、「堅牢で柔軟な UI を容易に構築可能」

How to

  1. 一度に 1 つのコンポーネントを構築

コンポーネントを個別にビルドし、関連する状態を定義する(小さく始める)

例:Avatar, Button, Input, Tooltip

  1. コンポーネントを結合

小さなコンポーネントを一緒に構成し、複雑さを徐々に増しながら、新しい機能をアンロック。

例:Form, Header, List, Table

  1. ページのアセンブル

複合コンポーネントを組み合わせてページを構築する。モックデータを使用して、到達しにくい状態やエッジケースのページをシミュレートする。

例:Home page, Settings page, Profile page

  1. ページをプロジェクトに統合

データを接続し、ビジネスロジックをフックすることで、アプリにページを追加する(サービスに統合/バックエンド API と連携)

例:Web app, Marketing site, Docs site

メリット

  • 品質

コンポーネントを分離してビルドし、関連する状態を定義することで、UI がさまざまなシナリオで動作することを確認可能。

  • 耐久性

コンポーネントレベルでテストすることで、バグを詳細に特定可能。テスト画面よりも作業が少なく正確。

  • 速度

コンポーネントライブラリまたは設計システムから既存のコンポーネントを再利用することで、UI を高速にアセンブルできる。

  • 効率性

UI を個別のコンポーネントに分解し、異なるチームメンバー間で負荷を共有することで、開発と設計を並列化できる。

ref: Component-Driven Development

standard tool

Component Story Format

  • シンプル: コンポーネント“ストーリー”の記述は、クリーンで広く使用されているフォーマットを使って ES 6 関数をエクスポートするのと同じくらい簡単。

  • 非独自仕様: CSF にはベンダー固有のライブラリは必要ない。

  • 宣言型: 宣言型構文は、MDX のような高レベルのフォーマットと同型であり、クリーンで検証可能な変換を可能にする。

Mock Service Worker

Mock Service Worker は API モックライブラリ

リモート API 呼び出しにあまり依存しないため、Mock Service Worker と Storybook の MSW アドオンを使用する

文字通り API のモックを定義できる

yarn init-msw

公式 Doc:https://mswjs.io/docs/getting-started/mocks/rest-api

golang で windows サービス 開発 (kardianos/service の 実装説明少々)

golangwindows サービス 開発 (kardianos/service の 実装説明少々)

前置き

golange で開発し、ビルドして生成した exe ファイルは、そのままでは

sc や NSSM (the Non-Sucking Service Manager) では、Windows サービス化はできても、起動ができない。

以下によると、kardianos/service を使えばよいとのこと。

Cannot start a Go application exe as a windows services

Windows サービスとして golang アプリケーション exe を起動できません

詳細な理由までは分かっていないが、windows サービス化するためには、それに必要な処理が実装されている必要があるらしい。

ref: エラー 1053:カスタムサービスが開始されません

golang には、windows サービス化 に必要となる処理も含む準標準ライブラリ(golang.org/x/sys)があり、ご丁寧にサンプルコードもある(https://pkg.go.dev/golang.org/x/sys@v0.0.0-20220702020025-31831981b65f/windows/svc/example

kardianos/service とは

kardianos/servicewindows の処理に関しては上記ライブラリを使用しつつ、win, linux, 等でサービス化を可能とするための共通のインターフェースを提供している。

  • OS 毎にサービス化するために必要な制御は異なる(特に Windows は大きく異なる)が、その制御処理は、kardianos/service に実装されており 共通のインターフェース (Service interface) を提供している。

https://github.com/percona/kardianos-service/blob/master/service.go#L292

  • Service interface を実装する windows 向けの service struct があるため、コンストラクタ(New 関数) に Start(), Stop() を実装したものを渡せばよい

https://github.com/percona/kardianos-service/blob/master/service_windows.go

  • コンストラクタ(New 関数) の引数には、Config がある。これには、サービス名や Description 以外にも、Working Directory、環境変数 等も設定することができる

https://github.com/percona/kardianos-service/blob/master/service.go#L85

  • コマンドライン引数はデフォルトで6つ用意してある。 install/unistall で Windows のサービス作成/削除ができ、Start/Stop で サービスの起動/停止ができる。

https://github.com/percona/kardianos-service/blob/master/service.go#L335

service_windows.go の詳細少々

service.Run() では、自身が用意した struct(以下では、exampleService)の Start と Stop が呼ばれる

https://github.com/percona/kardianos-service/blob/master/service_windows.go#L247

コマンドライン引数に start を指定すれば 自身が用意した Start() が呼ばれ、stop を指定すれば、自身が用意した Stop() が呼ばれる訳だが、その仕掛けが以下の通り。

  • コマンドライン引数に install を指定すれば、windows サービスが作成されるが、その際の実行ファイルのパスのデフォルトは 自身(作ったコードをビルドして生成した exe ファイル)。

https://github.com/percona/kardianos-service/blob/master/service_windows.go#L190

https://github.com/percona/kardianos-service/blob/master/service_go1.8.go#L10

  • start した際、exe ファイルが 引数なしで実行されることになる。つまり、Run()が実行され、自身が用意した Start()が呼ばれる。そしてプロセスが終了するまで待ちに入る

https://github.com/percona/kardianos-service/blob/master/service_windows.go#L271

  • stop した際は、windows サービス停止が行われ、(恐らく)それに伴い終了シグナルが発行され、上記で待ちに入っていた箇所でその通知を受けることで、待ちが終わり、後続に控える Stop() の処理が行われると思われる

https://github.com/percona/kardianos-service/blob/master/service_windows.go#L338

少々分かりにくいが、よくできた仕掛けに思う

type exampleService struct {}
func (e *exampleService) Start(s service.Service) error {
  // implement service start
  return nil
}
func (e *exampleService) Stop(s service.Service) error {
  // implement service start
  return nil
}

func main() {
    svcConfig := &service.Config{
        Name:        "ExampleService",
        DisplayName: "ExampleService (Go Service Example)",
        Description: "This is an example Go service.",
    }

    // Create Exarvice service
    program := &exarvice{}
    s, err := service.New(program, svcConfig)
    if err != nil {
        log.Fatal(err)
    }

    // Setup the logger
    errs := make(chan error, 5)
    logger, err = s.Logger(errs)
    if err != nil {
        log.Fatal()
    }

    if len(os.Args) > 1 {
        err = service.Control(s, os.Args[1])
        if err != nil {
            fmt.Printf("Failed (%s) : %s\n", os.Args[1], err)
            return
        }
        fmt.Printf("Succeeded (%s)\n", os.Args[1])
        return
    }

    // run in terminal
    s.Run()
}

ソース

ログファイルへの出力も可能か試したが、可能であった

https://github.com/Symthy/golang-practices/tree/main/go-win-service

refs

  • kardianos/service 使用サンプル

Go 言語で Windows の Service を作成する 2018/06

Go 言語で Windows,Linux の常駐システムを開発する

go で Windows service を作成する 2014/12

  • その他

Go 言語 - Windows 上でのプロセス存在チェック

go-ini では セミコロンが省略される

go-ini では セミコロンが省略される

前置き

Windows環境変数の区切り文字は、; = 設定ファイル(内容:= の形式)で、Windows環境変数を設定する必要がある場合は ; を使用

しかし、; がある設定ファイルを go-ini で読み込んだら、; が欠けて読み込まれた。

理由

go-ini では、; はコメントのシンボル。

ソースを見ると、コメントシンボルとして扱っている箇所があることが分かる

https://github.com/go-ini/ini/blob/14e9811b1643cf01ea36277e44dffef4f119fa31/parser.go#L432

go-ini の issues にも本件に関するものがある

https://github.com/go-ini/ini/issues/169

上記 issue にある通り、以下に回避方法が書いてある。

https://ini.unknwon.io/docs/howto/work_with_comments

Golang ビルド制約使用時の golps に関する注意事項

Golang ビルド制約使用時の golps に関する注意事項

前置き

プラットフォーム毎の処理を実装するときは、runtime.GOOS での判定は NG、 ビルド制約を使うのが吉

Golang はどのようにクロスプラットフォームの開発とテストを簡素化するのか

vscode + gopls で ビルド制約 を扱う

gopls では、ビルドタグを、2 つ以上同時には設定できない。

1 つしか設定できない= linux,windows 等複数プラットフォームに対する個別処理を実装する際には、随時 buildFlags を切り替えるしかなさそう。

ref: x/tools/gopls: improve handling for build tags #29202

なので、ワークスペースの設定ファイル作り、以下のように、buildFlags を windows, linux 切り替える shell 等 を用意して1コマンドで切り替えられるようにすれば、多少手間は省けるかと。

※ただし、ビルド制約を使用しているソースに関しては、例えば buildFlags を windows にしている時は それ以外のプラットフォーム用のソースで 警告かエラーが必ず出てしまう。 (そこに関しては妥協するしかないのだろうか…)

VSCODE_SETTINGS=".vscode/settings.json"
TAGS_WIN="\"-tags=windows\""
TAGS_LINUX="\"-tags=linux\""
REGEX_WIN=".*${TAGS_WIN}.*"
REGEX_LINUX=".*${TAGS_LINUX}.*"

build_flag_line=`grep "build.buildFlags" ${VSCODE_SETTINGS}`

if [[ ${build_flag_line} =~ ${REGEX_WIN} ]]; then
    echo "chage windows to linux"
    sed -i -e "s/${TAGS_WIN}/${TAGS_LINUX}/g" ${VSCODE_SETTINGS}
elif [[ ${build_flag_line} =~ ${REGEX_LINUX} ]]; then
    echo "change linux to windows"
    sed -i -e "s/${TAGS_LINUX}/${TAGS_WIN}/g" ${VSCODE_SETTINGS}
else
    echo "no change"
fi

cat ${VSCODE_SETTINGS}

PlantUML ガントチャート を活用してスクラムでのプチ進捗管理を行い見える化を進めたい

PlantUML ガントチャート を活用してスクラムでのプチ進捗管理を行い見える化を進めたい

背景/前提

  • 立ち位置:2 次請け側、プロダクト開発案件に自社が1サブチーム(6~7 人)として参画(=プロダクトを行っている会社が我々から見れば顧客)
  • スクラム開発。1 Spint 2 週間。
  • メンバーの実力が低く、頭2つ3つ抜けてる1人(私)がカバーに入らざるを得ない状況が毎 Sprint 簡単に起きている
    • 他メンバーのカバーに(私の)リソースが割かれすぎている
      • 私しかできない重いタスクに数日程集中せざるを得なくなり、カバーにあまり入れなかった際に、明らかに一部メンバーの進捗が悪くなった
      • 毎月残業上限ギリギリで、改善等に回せるリソースが残らない
      • 毎 Sprint でチームとしてこれだけのタスクやりますと顧客と交わしている以上、チームとしてそれに対する成果(タスク完了)を出さなければならない。カバーに入らなければ 1 sprint でこなす予定のタスクが終わらない(それでも毎回若干はみ出しているためなんとも…)
        • 放っておいて(というよりは必要に応じて適度かつソフトな支援を行う)で進むならわざわざ介入したりはしない。
        • マイクロマネジメントにならないように、悪い点を指摘するのではなく次に繋げるように伝えるよう心掛けているが、できてない部分はあると思われる。
    • 根本の解決にはメンバーの実力の底上げが必要だが、それがひどく難しい状況。
      • 週 1 で 30 分勉強会を 1 年近く(7,8 割私主導で)開催し、知識の共有を図っていたが、それが無駄という判断をせざるを得ない状況に追い込まれ、止める決断をした程
      • 外部でうまくいった事例を適用できれば確かに向上ができそうと思っても、組織/体制的に適用が困難だったりするため、有効な手立てが見つからない状況から脱せず
  • 実力底上げが困難と判断している、かつ悪循環が起きている以上、別方面から何かしら手を打たなければならない
    • カバーに入らざるを得ない状況に追い込まれるのは必然(そう簡単に解消は無理)と割り切った上で、それを少なくする、もしくは先回りして潰して、時間を減らす必要がある
  • 現状何が悪いのか?何に多くの時間が掛かっているのか?そこの特定がしきれていない
    • 毎 Sprint 各々の振り返りが浅いままで終わっている
    • 聞いていて引っかかった所は質問を投げて深堀を試みるが、見えてない/気づけてない物は多分拾えていない
    • 一番深堀できる人(私)が、見えてない/気づけてないから、深堀しきれずに終わってる(他メンバーよりできるだけであって、うまいとは言っていない)

故に全体の見える化をする必要がある。

ガントチャートなりで予実績の記録+ α を行い、記録を徹底する必要があると考える

PlantUML のガントチャートを使ってみる

ツールを導入したり等できるなら、そうしたいができないので、図を作ってまずは全体の俯瞰視ができるようにする。

図を作るものとしての候補は、

  • PlantUML
  • Mermaid

最近は、Github 等の色々なサービス が Mermaid をサポートし、Mermaid の方が注目を浴びているように感じる。

見た目だけで言えば、個人的には、Mermaid > PlantUML だし、純粋なガントチャートが作りたいなら Mermaid で良かったんだが、作りたいのは、純粋なガントチャートではなく…

PlantUML の方が頭の中でイメージしていたものに限りなく近いものが作れるため、PlantUML を選択。

以下が、考えた雛形。(タスクを1列に並べたり、付箋(note)付けるのは PlantUML でないとできない)

以下ソース。

@startgantt
language ja

printscale daily zoom 6
<style>
ganttDiagram {
    task {
        'FontColor red
        FontSize 16
        FontStyle bold
        BackGroundColor Azure
        LineColor DodgerBlue
    }
    milestone {
        'FontColor blue
        FontSize 16
        'FontStyle italic
        BackGroundColor black
        LineColor black
    }
    note {
        FontSize 12
    }
    arrow {
        LineColor black
    }
    separator {
        FontSize 18
        FontStyle bold
        FontColor black
    }
    timeline {
        BackgroundColor AntiqueWhite
    }
    closed {
        BackgroundColor pink
        FontColor red
    }
}
</style>

Project starts 2022/07/01
saturday are closed
sunday are closed
'2022/xx/xx is closed
'2022/xx/xx to 2022/xx/xx is closed

[Sprint XX] lasts 2 weeks
[Sprint XX] is colored in LightGreen
[Sprint XX] links to [[http://example.com]]

'[Task 1 (1日)] lasts 1 day
'[T2 (5日)] lasts 5 days
'[T3 (1週間)] lasts 1 week
'[T4 (1週間と4日)] lasts 1 week and 4 days


-- Person A --

'Task A
[Task A] happens at [Sprint XX]'s start
[A 検討] lasts 2 days
note bottom
・こういう点が良くなかったよね
end note
[A 環境構築] lasts 1 days
note bottom
・どこどこに時間がかかった
・ここはこうした方が良かった
end note
[A 実装] lasts 4 days
[A PR] lasts 2 days
note bottom
・こうすれば指摘減らせたよね
end note

[A 検討] -> [A 環境構築]
[A 環境構築] -> [A 実装]
[A 実装] -> [A PR]
[A 環境構築] displays on same row as [A 検討]
[A 実装] displays on same row as [A 検討]
[A PR] displays on same row as [A 検討]

[A 検討END] happens on 2022/7/4
[A 環境構築END] happens on 2022/7/6
[A 実装END] happens on 2022/7/12
[A END] happens on 2022/7/14
[A 実装END] displays on same row as [A 検討END]
[A 環境構築END] displays on same row as [A 検討END]
[A END] displays on same row as [A 検討END]

-- Person B --
'Task B
[Task B] happens at [Sprint XX]'s start
[B 検討] lasts 1 days
[B 実装] lasts 3 days
[B PR] lasts 2 days
[B 検討] -> [B 実装]
[B 実装] -> [B PR]
[B 実装] displays on same row as [B 検討]
[B PR] displays on same row as [B 検討]

[B 検討END] happens on 2022/7/4
[B 実装END] happens on 2022/7/6
[B END] happens on 2022/7/11
[B 実装END] displays on same row as [B 検討END]
[B END] displays on same row as [B 実装END]

-- --
'Task C
[Task C] happens at [Sprint XX]'s start
[C 実装] pause on 2022/7/1 to 2022/7/8
[C 実装] lasts 2 days
[C PR] lasts 1 days
[C 実装] -> [C PR]
[C PR] displays on same row as [C 実装]

[C 実装END] happens on 2022/7/12
[C END] happens on 2022/7/14
[C END] displays on same row as [C 実装END]

-- --

@endgantt

ツールを作ればいい

これを手書きで作るのは面倒なため、python + jinja2 等で、雛形生成ツール的なものを作ればよさそう。

追記:4 時間程で作った https://github.com/Symthy/sprint-gantt-tmpl-builder

GUI 操作のみで容易に編集可能にできれば、もっと楽になるとは思う。ガントチャート+ α なフロントエンドアプリでも作れればと思うが、フロントエンドアプリ開発は勉強中のため、やるにしてもそれは大分先の話になるだろう。

外部ツールが使えない/適当な物がなければ、まずは自身のできる範囲でできること(ツールなり作る)をすればいい。効率化/課題解決はエンジニアの腕の見せ所

さいごに

前提を書いていて虚しさみたいな物が込み上げてきて辛かったが、自身が立ち向かおうとしている物や現状を正しく認識するためにも言語化は必要

いつかは抜け出すことを画策しているとしても、今のこの逆境に真向から立ち向かう(自身が向き合いたいものと違ったとしても)。その姿勢と取り組みはきっと次に繋がると信じて。

Job: Golang で ログローテートと設定ファイル読み込み改修 取捨選択&実装

Job: Golang で ログローテートと設定ファイル読み込み改修 取捨選択&実装

自らの立ち回り、検討事項、実装コードを記す

背景

これまで Linux のみサポートの製品(言語:Golang)を 別プラットフォームに対応する必要がでてきた

  • Linux 依存の部分(ログローテート等)があったため対応が必要
    • ログローテートも Golang で実装
  • 第一候補として挙がったのが Golang のログローテートで有名な lumberjack

Golang はどのようにクロスプラットフォームの開発とテストを簡素化するのか

着手前の状態

  • ログローテートには Linux の logrotate.d が使用されていた

  • 設定ファイルは、systemd のサービスユニットファイルで EnvironmentFile=<value> により環境変数に展開して使用

蛇足:Linux に関するちょっとした Tips

(コンテナ運用ではなく) Linux の systemd のサービスユニットファイルで設定した環境変数は、そのサービスのみで使用される private な環境変数となる=他へ影響を及ぼさない/他からの影響を受けない

[Service]
Environment=<環境変数リスト>
EnvironmentFile=<環境変数ファイル>

これを golang では、以下のみで取れる(指定の key がなければ空文字が取れるだけ)

os.Getenv("key")

これの何が良いかと言うと、システムのグローバル変数として使うことができる

共通のグローバルな値を golang のコード上で定数/変数を定義した場合、メモリを占有し続けることになる。少量なら問題ないかもしれないが、積もり積もって、ガーベージコレクション時のオーバーヘッドが高くなれば、性能劣化に繋りかねない。(参考:Go の GC のオーバーヘッドが高くなるケースと、その回避策

ただし、イミュータブルにすることはできずグローバルな「変数」であることに変わりないため、取り扱いには気を付ける必要がある。たとえば

  • 定数として扱うように規約を定める、仕組化する
  • グローバル変数として扱うなら、Repository 層のように取得/更新のインターフェースを定め、状態の管理を徹底する

など。

取り組み

ログローテート

lumberjack が第一候補として挙がっていたが、ちょっとした問題点が見つかる。

  1. ローテートの契機が (logrotate.d とは) 異なる
  2. ローテートした後のファイル名のフォーマットが異なる

詳細:

logrotate.d lumberjack
1. ローテート契機 1 日 1 回ローテート(時、週、月毎等変更可) 最大サイズ(default:100MB)を
超過したらローテート
2. ローテート後のファイル名 末尾に付ける日付のフォーマット指定可(<ログファイル名>.log<指定フォーマット>) 固定 (<ログファイル名>-yyyy-MM-ddTHH:mm:ss.fff.log)

「1. ローテートの契機が異なる」について

念のため、lumberjack で日毎にローテートする機構があったりしないか、コードも見て確認。 だが、以下の通り、ローテートするかは最大サイズしか見ていなかった (2022/6 時点)

   if info.Size()+int64(writeLen) >= l.max() {
        return l.rotate()
    }
   if l.size+writeLen > l.max() {
        if err := l.rotate(); err != nil {
            return 0, err
        }
    }

できれば、ローテート契機を変えずに済む OSS があればと思うが、

golang のログローテートといえば、lumberjack というイメージがあり、(以前プライベートで)日本語でググった際に他の有力な OSS を見かけたことがないため、存在していたとしてもマイナー寄りかもしれない。

そう思い、英語情報を漁らないと見つからない可能性があると判断。「golang log rotate daliy」で検索するも、

商用で使えるレベルのものは見つからなかった。

  • 唯一使えそうに思えたのは以下。ただしアーカイブされていた
  • 他は個人の方が作成されたようなもの位しか見つけられず

よって、lumberjack を使わず logrotate.d と同等のログローテートを実現しようとすると、自分達で作るしかない。例えば

  • (ライセンス的に問題のない) OSS のコードを拝借する等して自分たちの持ち物とし、自作する
  • lumberjack をフォークして日毎に 参考:Daily Rolling Logger

ただ、自作したとして

  • コスト(開発コスト、以降のメンテナンスコスト等)に対するリターンが、見合わない
  • lumberjack を使うにあたり、1 面あたりの容量を 1 日以上持てる大きさにすれけば運用上もそう困ることはない

という点から lumberjack で行くことにした。

「2. ローテートした後のファイル名のフォーマットが異なる」について

これにより何が起きるか

  • logrotate.d でローテートされたログファイルを lumberjack には引き継がれない(無視する)ため、そのまま残り続ける

できれば lumberjack に引き継がれて、古いものから自然に削除されて欲しい。そこで考えた。

logrotate.d で作成されたファイル名を lumberjack がローテート時に付けるファイル名に合わせれば、lumberjack の仕組みに乗っかり、ローテートされるのはないかと推測。

lumberjack のソースを見る限り、この推測は正しそう。(該当箇所ソース:https://github.com/natefinch/lumberjack/blob/47ffae23317c5951a2a6267a069cf676edf53eb6/lumberjack.go#L400

ファイル名を一括置換する shell を組んで実行。実機で lumberjack を使ったコードで動かして試してみると、推測通りに動き、この問題は回避できた。

設定ファイル

設定ファイルは、Linux のサービスユニットファイルで環境変数に読み込んで使用する。という Linux 依存の部分を golang の処理で設定ファイルを読み込むようにして、別プラットフォームに対応する。

設定ファイルの読み込み処理は新規に実装する必要があるため、挙げた選択肢は以下3つ。

  1. go.ini を利用
  2. godotenv を利用
  3. 別サービスの既存コードを流用

結論を先に述べれば、選んだのは「go.ini を利用」。他を除外した理由はそれぞれ以下の通り。

  • godotenv

読み込んだファイルを環境変数にセットしてくれ、os.Getenv で変わらず取得できる OSS。これが製品の既存コードとの親和性とも高いため、ベストな選択肢ではと最初は考えていた。

godotenv

だが、以下のように書かれており、動作の保証ができないため、使用を断念。

There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows.

翻訳結果:Linux 環境と Windows 環境の両方でテストカバレッジと CI がありますが、Windows で動作する bin バージョンについては保証しません。

  • 別サービスの既存コード

あるにはあったレベル。理想は、共通部品として切り出して両者でそれを利用するようにするのが良いとは考えた。だが…

今すぐそれを行える余力が(組織的に)なく、かといって同じコードを量産したくない(流用したコードに何かあれば両者共に直す必要が出てくる可能性)。また、戻り値が (map[string]string, error) 関数のため、そのままでは少し扱いづらく改良も必要。

実装

故に go.ini が安牌と考え、採用して実装した。

とはいえ、そのまま使用して製品コードと go.ini を密結合にさせると、不測の事態で go.ini から別の OSS または自作ソースへの乗り換えが発生した場合に、手間がかかってしまう。

そこで、interface を定義してラップし、疎結合とすることで、その手間を軽減する。

type Config interface {
    GetString(key string) string
    GetInt(key string) (int, error)
}

type GoIniConfig struct {
    file *ini.File
}

func (c GoIniConfig) GetString(key string) string {
    return c.file.Section("").Key(key).String()
}

func (c GoIniConfig) GetInt(key string) (int, error) {
    return c.file.Section("").Key(key).Int()
}

また、設定ファイルの構成は、(よくある?)システム用とユーザー用の2つあり、値の優先度は

  • ユーザー設定ファイル > システム設定ファイル > デフォルト値

のため、そこもいい感じにコードで表現する。

type ConfigValueResolver struct {
    systemConf Config
    userConf   Config
}

func (c ConfigValueResolver) ResolveValueStringOrDefault(key string, defaultValue string) string {
    value := defaultValue
    value = getConfValueStringOrElse(c.systemConf, key, value)
    value = getConfValueStringOrElse(c.userConf, key, value)
    return value
}

func getConfValueStringOrElse(conf Config, key string, defaultValue string) string {
    // key がない場合は "" が返ってくる
    if confValue := conf.GetString(key); confValue != "" {
        return confValue
    }
    return defaultValue
}

コードの全体イメージ(テスト含む)は こちら(Github)

※テストには testify の mock を使用することで、設定値の優先度(ユーザー設定ファイル > システム設定ファイル > デフォルト値)のテストを実装し、実ファイルに依存することなくコード上で全テストケース表現(そのための Config の interface 化でもある)。また、各テストケースがどういう内容か見づらいため、 Go のテーブル駆動テストをわかりやすく書きたい を参考に見やすくしてみた。

蛇足

lumberjack.Logger を log.SetOutput() でセットしたり、フレームワークEcho を使用していたため、そちらのロガーに渡したりする必要があった。

が、lumberjack.Logger のインスタンス生成する前に、設定ファイルから必要な値を取得する必要がある。でも設定ファイル読み込み失敗等はログに出力する必要があるという矛盾…

なので、関数を使って簡易バッファリング。log.SetOutput() してから、まとめて出力。

LoadConfigFile(filePath string, logBufferedWriters []func()) (Config, []func()) {
    cfg, err := ini.Load(filePath)
    if err != nil {
        logWriters := append(logBufferedWriters, func() {
            log.Printf("Failed to file: %v\n", err)
        })
        return nil,
    }
    return &ConfigFile{file: cfg}, logBufferedWriters
}

※関数は便利。なんとなく方法として微妙な気がするが、良い方法を思いつかない。精進

最後に

実務で golang を初めて使う機会(一ヶ月間)を得て、それから半年強のブランクを経て、再度その機会が巡ってきた。

当時は、基本構文+ α さえ押さえておけばなんとかなるレベルであったため、事前にそのレベルまでの習得に留めたが、それからポートフォリオを作ろうとプライベートで半年書き続けていた甲斐あり、ある程度スムーズにこなすことができた(途中割り込みありありで 700step 強/3 日、速くはないか…)。

地道な努力が功を奏したと思える瞬間。だがまだ精進が必要。チャンスを掴み取るためにも地道に積み重ねるしかない。

Golang 並列処理(Gorutine/Channel/WaitGroup/ErrorGroup)

Golang 並列処理(Gorutine/Channel/WaitGroup/ErrorGroup)

Go の魅力

  • 他の言語は後から並列処理の機構を組み込むと大手術になることがあるが、Go は容易
  • 高水準のパフォーマンスが出るコードを少ない手間で実現できるところ
  • I/O コストが高い領域は Go との相性が良い

Go の業務アプリケーションで並列処理の適用を検討すべき場面は、1 リクエスト/バッチタスクの内部を高速化したい時。(例:1リクエスト中で複数データストアから情報取得し、結果を複数箇所に格納が必要な時)

Gorutine

  • 個々の goroutine は識別不可
  • 優先度や親子関係はない
  • 外部から終了させられない
  • 終了検知には別の仕組みが必要(channel?)
  • かなり少ない量のメモリしか要求せず、起動は高速
    • 起動コストはゼロではない

goroutine の乱用は避ける

  • 並列処理は複雑さと高める
  • goroutine を駆使したコードは意図が伝わりにくい
  • 基本的には標準/準標準パッケージ機能の利用を検討
func main() {
    go output("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("immediate execution")
}

Channel

  • 同時実行する goroutine を接続するパイプ(複数の goroutine から送受信しても安全が保障されたキュー)
  • Channel は同期の手段
    • Channel は goroutine をブロックする
    • 送信 goroutine と受信 goroutine が揃うまでブロック(バッファなしの場合)
    • 送信側のバッファ一杯になると受信側が取りに来るまで or バッファが空ならブロック(バッファありの場合)

※ブロック=待ち受け

func main() {
    msgs := make(chan string, 3)
    msgs <- "main"
    go func() {
        msgs <- "func1"
    }()
    go func() {
        msgs <- "func2"
    }()

    msg1 := <-msgs  // channelから値を読み込むまでメインgoroutineストップ
    msg2 := <-msgs
    msg3 := <-msgs
    fmt.Println(msg1, msg2, msg3)  // main func2 func1
}
  • 一方向チャネル型
    • チャネルの向きを指定できる。向き:送信/受信
func send(recvCh chan<- string, msg string) {
    recvCh <- msg
}

func receive(sendCh <-chan string, recvCh chan<- string) {
    msg := <-sendCh
    recvCh <- msg
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    // send(ch1, "sending")  deadlock..
    // receive(ch1, ch2)
    go send(ch1, "sending")
    go receive(ch1, ch2)
    fmt.Println(<-ch2)
}
  • キャパシティが一杯のチャネルに書き込もうとするゴルーチンは、チャネルの空きが出るまで待機する
  • 空のチャネルから読み込もうとするゴルーチンは、チャネルに要素が入ってくるまで待機する
func main() {
    // send slow
    ch1 := make(chan string, 2)
    go func() {
        for i := 0; i < 6; i++ {
            send := "send" + strconv.Itoa(i)
            ch1 <- send
            time.Sleep(1 * time.Second)
        }
    }()
    go func() {
        for j := 0; j < 3; j++ {
            fmt.Println("sub:", j, <-ch1)
        }
    }()
    for j := 0; j < 3; j++ {
        fmt.Println("main:", j, <-ch1)
    }
    time.Sleep(3 * time.Second)
}
//main: 0 send0
//sub: 0 send1
//main: 1 send2
//sub: 1 send3
//main: 2 send4
//sub: 2 send5

Select

複数チャネルの待ち受けかつチャネル毎の制御ができる。

  • 複数 goroutine の待ち受け可
  • 先に終わったものから捌く
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    // ch3 := make(chan string)
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("received1", msg1)
        case msg2 := <-ch2:
            fmt.Println("received2", msg2)
        // default:
        // fmt.Println("none")
        // default句を入れると待ち受けが起こらず全てnoneが出力
        // default句は何も送受信がなかった時の処理
        // goroutine起動のタイムラグによるすり抜け
        }
    }
}
// received1 one
// received2 two

timeout

func main() {

    ch1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "sending 1"
    }()

    select {
    case res := <-ch1:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout 1")
    } // timeout

    ch2 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "sending 2"
    }()
    select {
    case res := <-ch2:
        fmt.Println(res)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout 2")
    } // success
}

Non-Blocking Channel Operation

  • バッファなし channel のため最初からブロック
  • <-readCh : Channel から値読み込みを永遠に待ち続ける = 値書き込みがなければ永続待機 = deadlock 発生
func main() {
    // var writeCh chan<- string
    var readCh <-chan string
    ch := make(chan string)
    readCh = ch

    go func() {
        // writeCh <- "Writing..."
    }()

    fmt.Println(<-readCh)
}
// fatal error: all goroutines are asleep - deadlock!

上記ケースを制御できるのが default

func main() {
    messages := make(chan string)
    signals := make(chan bool)

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    default:
        fmt.Println("no message received")
    }

    msg := "hi"
    select {
    case messages <- msg:
        fmt.Println("sent message", msg)
    default:
        fmt.Println("no message sent")
    }

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    case sig := <-signals:
        fmt.Println("received signal", sig)
    default:
        fmt.Println("no activity")
    }
}

close

(チャネル受信待ちなど) ブロック中の goroutine を解放

func main() {
    jobs := make(chan int, 5)
    done := make(chan bool)

    go func() {
        for {
            j, more := <-jobs
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("received all jobs")
                done <- true
                return
            }
        }
    }()

    for j := 1; j <= 3; j++ {
        jobs <- j
        fmt.Println("sent job", j)
    }
    close(jobs) // ループも解除
    time.Sleep(time.Second)
    fmt.Println("sent all jobs")

    <-done
}

クローズすることで、range を用いて channel から取り出すことができる

※クローズしなければ受信待ちによるブロックで deadlock

func main() {
    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue) // 削除するとdeadlock

    for elem := range queue {
        fmt.Println(elem)
    }
}

worker

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker", id, "finished job", j)
        results <- j * 2
    }
}

func main() {
    const jobNum = 5
    jobs := make(chan int, numJobs)  // ★バッファ有無で挙動が変わる
    results := make(chan int, numJobs)  // バッファ指定なくすとdeadlock

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    // go func() {
    for j := 1; j <= jobNum; j++ {
        jobs <- j
    }
    // }
    fmt.Println("sleep...")
    time.Sleep(2 * time.Second)
    fmt.Println("sleep end")
    close(jobs)

    for a := 1; a <= jobNum; a++ {
        fmt.Println(<-results)
    }
}
// ★ バッファ指定なしの場合
// worker 3 started  job 1
// worker 1 started  job 2
// worker 2 started  job 3
// worker 1 finished job 2
// worker 1 started  job 4
// worker 2 finished job 3
// worker 2 started  job 5
// worker 3 finished job 1
// sleep...
// worker 1 finished job 4
// worker 2 finished job 5
// sleep end
// 4
// 6
// 2
// 8
// 10

// ★ バッファなしでも jobs <- j のループを別goroutineにすると以下
// sleep...
// worker 1 started  job 1
// worker 2 started  job 2
// worker 3 started  job 3
// worker 3 finished job 3
// worker 3 started  job 4
// worker 2 finished job 2
// worker 2 started  job 5
// worker 1 finished job 1
// sleep end
// 6
// 4
// 2
// worker 3 finished job 4
// 8
// worker 2 finished job 5
// 10

// ★ バッファ指定ありの場合
// sleep...
// worker 1 started  job 1
// worker 2 started  job 2
// worker 3 started  job 3
// worker 3 finished job 3
// worker 3 started  job 4
// worker 1 finished job 1
// worker 1 started  job 5
// worker 2 finished job 2
// worker 1 finished job 5
// worker 3 finished job 4
// sleep end
// 6
// 2
// 4
// worker 1 finished job 5
// 10
// worker 2 finished job 4
// 8
  • バッファ指定ありの場合は、待ち受け? バッファが空だからブロック、送信されたものを worker が受け取るまでのタイムラグ?の間に main の sleep 実行か?
  • バッファ指定なしの場合は、即実行? 送信側(main)と受信側の goroutine 揃うためブロックされず、worker が受け取ったら即実行
    • 送信側も goroutine にすると、起動のタイムラグで先に main の sleep 実行
  • results をバッファ指定なしにした場合は、3 回送信時点で受信側がそれ以上取り出そうとし deadlock

fan-out/fan-in

  • ファンアウト:並列処理の起点となる1つのロジックから分岐。この分岐
  • ファンイン:分岐の待ち合わせ

sync.WaitGroup

  • ファンアウト、ファンインの仕組みを提供
  • 複数の goroutine を管理

メソッド:

  • Add:タスク数登録
  • Done:タスク完了
  • Wait:タスク完了の待機
func worker(id int) {
    fmt.Printf("Worker %d start\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d end\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        i := i
        go func() {
            defer wg.Done()
            worker(i)
        }()
    }
    wg.Wait()
}

Channel で同様にデータの受け渡しが可能

func responseSize(wg *sync.WaitGroup, url string, nums chan int) {
    defer wg.Done()
    response, err := http.Get(url)
    if err != nil {
        log.Fatal(err)
    }
    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    nums <- len(body)
}

func main() {
    wg := new(sync.WaitGroup)
    nums := make(chan int)
    wg.Add(1)
    go responseSize(wg, "https://www.example.com", nums)
    fmt.Println(<-nums)
    wg.Wait()
    close(nums)
}

errorgroup.Group

  • 基本形
func main() {
    var eg errgroup.Group
    for i := 1; i <= 5; i++ {
        id := i
        eg.Go(func() error {
            worker(id)
            return nil
        })
    }

    if err := eg.Wait(); err != nil {
        fmt.Println("error: ", err)
    }
}
  • エラーが発生したときに後続の Goroutine をキャンセルする(context)
func main() {
    eg, ctx := errgroup.WithContext(context.Background())

    for i := 0; i < 100; i++ {
        i := i
        eg.Go(func() error {
            time.Sleep(2 * time.Second) // 長い処理

            select {
            case <-ctx.Done():
                fmt.Println("Canceled:", i)
                return nil
            default:
                if i > 90 {
                    fmt.Println("Error:", i)
                    return fmt.Errorf("Error: %d", i)
                }
                fmt.Println("End:", i)
                return nil
            }
        })
    }
    if err := eg.Wait(); err != nil {
        log.Fatal(err)
    }
}

refs

良き Example

Groutine/Channel

WaitGroup/ErrGroup

golang 並列処理のはまりどころ

詳細な解説

Concurrency Patterns