リーダブルな文章を書くためのおすすめ書籍3選
文書の分かりにくさは「表現の分かりにくさ」と「文脈の分かりにくさ」に大別できる1。本記事では主に前者の「表現の分かりにくさ」を改善するためにおすすめできる書籍を紹介する。
後者の「文脈の分かりにくさ」は読み手に依存する。日本語としては理解できるが頭に入ってこない文章、が存在するのは直感的に受け入れられるだろう。読み手の前提を把握し、読み手の論点に向かって文章を書く必要があるが、本記事ではここには踏み込まない。
1.理工系のためのよい文章の書き方
よい文章を書くために必要な全体像がつかめる。原則、文章構成や文の書き方、心得などを説明している。190ページ程度で簡潔にまとまっているのが良い。書いてあることは基本的な内容であり、著者も「どちらかというと学生向けを意識して書いたものである2」と述べているが、多くの社会人にとっても役に立つだろう。
『理科系の作文技術3』も本書同様に文章構成、主題の決め方など、よい文章や論文、レポートを書くために必要な項目を網羅的に解説しているが、冗長と感じている。基本的な文章の書き方を学びたいメンバーには『理工系のためのよい文章の書き方』をすすめたい。
2.日本語作文術
特に第一章の「作文術の心得 ―短文道場―」が良い。分かりやすい文を書く技術が学べる。以下のような意識しやすい、あるいは実践しやすい内容が豊富に記載してある。
- 考えがしっかりまとまっていないから、文が長くなる
- 「音読」は文章を推敲するときに非常に大切なことである
- 読みづらい箇所はたいてい文の組み立て方か、言葉づかいに難点がある
- 分かりやすい文章を書く上でいちばん大切な心構えは「読み手の身になって書け」ということだ
- ハとガの使い分けは文法的な問題ではなくて、使う人の視点(スタンス)の問題ということだ
3.文章力の基本
『日本語作文術』の短文道場、あるいは『理科系の作文技術』の第8章(わかりやすく簡潔な表現)をより平易に解説しているような書籍である。悪い例と良い例を示し、改善ポイントは何であるかを解説している。例が豊富でイメージしやすく、入門書として良い。難点は、悪い例がわざとらしいというか、いかにも初心者の文と感じるものが多いことである。『日本語作文術』には「文章指南書がよくやる素人の作例は絶対に使わない。あれはやっている本人はご満悦かもしれないが、一種の弱いものいじめだ」とある。4『文章力の基本』での例はそれに近い。好き嫌いは分かれそうである。
- https://blog.tinect.jp/?p=60953↩
- https://twitter.com/kentarofukuchi/status/1594484904766500864↩
- https://www.chuko.co.jp/shinsho/1981/09/100624.html↩
- ちなみに『日本語作文術』でも悪い文を例示し、良い文に改善する方法を紹介している。ただし、例として挙げているのは素人の文章ではなく、横溝正史、鈴木大拙といった日本の文豪が書いた文章である。↩
HIGH OUTPUT MANAGEMENTのミーティングの章メモ
はじめに
普段の仕事の中でミーティングをする機会はとても多いです。朝会などのチームメンバで毎日実施するようなミーティングや、週次で実施しているプロジェクトの定例、顧客との定例、関係者と業務やシステムの仕様を調整するようなミーティング、などなど様々あります。簡単に開けてしまうミーティングですが、ミーティングの場を有意義な時間にするには、ミーティングの目的やミーティングの中で何を実施しなければいけないのか、どのような準備が必要なのか考慮することはたくさんあります。
本記事では HIGH OUTPUT MANAGEMENT で紹介されているミーティングの章からミーティングには同様な種類があるのか、何を行うのか紹介します。書籍を読んだメモです。
ミーティングの種類
ミーティングの種類について HIGH OUTPUT MANAGEMENT では以下にように分類しています。
大きくミーティングは2種類に大別でき、知識の共有化や情報を交換を目的とした「プロセス中心」のミーティングと、具体的な問題解決を目的とした「使命中心」のミーティングがあります。
プロセス中心のミーティング
プロセス中心のミーティングは大きく3つ存在する(インテル社の場合)。
- 1on1
- スタッフミーティング
- 業務検討会
である。
1on1
- どのようなミーティングか
- 監督者と部下の間のミーティング。部下のためのミーティング
- 目的
- 相互に教えたり、情報を交換すること
- ミーティングの構成/議題
- 部下が直面していることや困っていること
スタッフミーティング
- どのようなミーティングか
- ある監督者とその部下全員が参加する部内会議
- 目的
- 同僚相互の交流、監督者が物事を知る機会
- ミーティングの構成/議題
- 出席者3人以上に関係する任意の事項
業務検討会
- どのようなミーティングか
- 相互に話し合う機会があまりない人々のための意見交換の場、手段
- 目的
- 組織階層が離れていて交流のない社員間で教育と学習を継続させようとするもの
- ミーティングの構成/議題
- 上級の管理者がプレゼンを実施
- 管理者の上司がアジェンダをフォロー
使命中心のミーティング
- どのようなミーティングか
- 特別な目的のために随時開かれるミーティング
- 目的
- 特定の成果、一定の意思決定に到達すること
- ミーティングの構成/議題
- 都合をつけて関係者を確実にミーティングに参加させる
GoでJSTのタイムゾーンを扱う方法
本記事はGoでJSTのタイムゾーンを指定する方法を紹介します。
現在時刻を取得するには time.Now()
を使うことになります。time.Now()
はデフォルトではローカルな時刻が取得できます。例えばAWS Lambda上ではUTCの時刻が取得できます。日本のロケーションで動作するアプリケーションを前提にすると、アプリケーションによっては時刻をJSTで統一したほうがシンプルで扱いやすい、といったケースもあるでしょう。本記事ではGoのアプリケーションでJSTの時刻を取得する方法を紹介します。
*time.Location
の取得方法
まずタイムゾーン (time.Location
) について説明します。Goではグローバル変数として *time.Local
変数があります。アプリケーションで更新することはアンチパターンとされているため、変数を上書きすることはないでしょう。
// Local represents the system's local time zone. // On Unix systems, Local consults the TZ environment // variable to find the time zone to use. No TZ means // use the system default /etc/localtime. // TZ="" means use UTC. // TZ="foo" means use file foo in the system timezone directory. var Local *Location = &localLoc
*time.Location
を取得するには主に以下のAPIを使います。LoadLocationFromTZData
を使用する機会は多くはないでしょう。
time.FixedZone
time.LoadLocation
LoadLocationFromTZData
time.FixedZone
を用いて *time.Location
を生成することができます。
myJST := time.FixedZone("Asia/Tokyo", 9*60*60)
または time.LoadLocation
でロケーション名を指定して *time.Location
を取得します。
jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) }
time.LoadLocation
は外部依存があるため scratch などの軽量コンテナ上では動作しません。ソースはランタイムごとに異なりますが、例えばUNIX系の場合は zoneinfo_unix.go から確認できます。
以下のようにして確認することができます。
- Dockerfile
FROM golang:1.15 as builder WORKDIR /go/src/app COPY go.mod go.sum ./ RUN go mod download COPY ./main.go ./ ARG CGO_ENABLED=0 ARG GOOS=linux ARG GOARCH=amd64 RUN go build -o /go/bin/main -ldflags '-s -w' -trimpath FROM scratch as runner COPY --from=builder /go/bin/main /app/main ENTRYPOINT ["/app/main"]
- main.go
package main import "time" func main() { _, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } }
- コンテナのビルド&実行
$ docker build -t jst-time-test1 -f Dockerfile . $ docker run --rm jst-time-test1
- 実行結果
panic: unknown time zone Asia/Tokyo goroutine 1 [running]: main.main() tzsample/light/main.go:8 +0x65
time.LoadLocation
を使ってタイムゾーンを取得する場合は、アプリケーションが動作するランタイム上で動作するか確認する必要があります。ランタイムにタイムゾーンの情報が含まれない場合は
などといった回避方法があります。
タイムゾーンを指定して時刻を取得する方法
続いて現在時刻を取得する方法です。time.Now()
で取得する時刻にロケーションを指定する方法は以下の2種類の方法があります。
※ "Asia/Tokyo"
のタイムゾーンを指定する文字列は環境変数から値を設定することももちろん可能です。
方法1.時刻をパースする際に In
でロケーションを指定する
もっともポピュラーな変換方法です。
jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } nowJST := time.Now().In(jst) fmt.Printf("nowJST: %v\n", nowJST.Format(time.RFC3339)) // nowJST: 2009-11-11T08:00:00+09:00
https://play.golang.org/p/BRrCCx4qpcP
方法2. time
パッケージが保持しているグローバル変数を更新する
time
パッケージが保持しているグローバル変数 time.Local
をJSTで更新する方法です。グローバル変数を更新することで間接的に time.Now()
で取得できるタイムゾーンを差し替えることができます。
func init() { jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } time.Local = jst } func main() { now := time.Now() fmt.Printf("now: %v\n", now.Format(time.RFC3339)) // now: 2009-11-11T08:00:00+09:00 }
https://play.golang.org/p/UkMP0dxlOyE
方法1と方法2を比較すると、方法1では時刻を取得するすべての実装箇所から In
を用いてタイムゾーンの変換をする必要があります。変換漏れがあると時刻がローカルな時刻とJSTと混在することになるため、一見方法2が一律指定できて、不具合が少ないと考えるかも知れません。
しかし方法2は筋がいい実装と言えません。もちろんミュータブルなグローバル変数を更新することで実現できますが、意図せずいろいろなところに影響が及ぶ可能性があります。実際にRuss Coxも次のようにほとんどの場合は間違いである、と述べています。
I've updated the title. What you're asking for is
``` package time
// SetLocal changes the current meaning of time.Local to match loc. func SetLocal(loc *Location) I don't believe this is a good idea. In general but especially in Go, coordination via global mutable state is almost always a mistake and difficult to correct once you realize it. ```
If there are higher-level APIs that force the use of time.Local, we should probably address those instead. For example, if package log is what you are concerned about, it would probably be okay to file a separate issue for adding log.SetLocation and log.(*Logger).SetLocation.
https://github.com/golang/go/issues/10701#issuecomment-148777945
実際、以下のように init
内で初期化した time.Local
の変数とテスト中に time.Local
を読み込むと DATA RACE が発生する、という事象もあります。私も再現しました。
go test -raceでinitで初期化したtime.Localのグローバル変数とtest中のtime.Localの読み込みがDATA RACEする問題にハマっとる。init()で初期化しているのでそもそもraceしないと思うし、time.Afterで参照しているtime.Localとバッティングしているのが謎…
— freedomな人 (@tzm_freedom) December 14, 2020
方法1'.ロケーション付で時刻が取得できる関数を生成
方法1の拡張版です。In
で変換するが、漏れのないように時刻を取得する関数を新たに生成して、その関数経由で時刻を取得する方法です。時刻を取得するときにアプリケーション側で宣言した nowFunc
を使う必要がありますが、方法2と比較すると安全な方法です。
var nowFunc func() time.Time func init() { jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } nowFunc = func() time.Time { return time.Now().In(jst) } } func main() { now := nowFunc() fmt.Printf("now: %v\n", now.Format(time.RFC3339)) // now: 2009-11-11T08:00:00+09:00 }
https://play.golang.org/p/uy6zILsixiZ
Songmu/flextime
などの時刻を差し替えることができるライブラリなどを用いれば、上記同様に時刻のタイムゾーン変換ができます。以下は Songmu/flextime
の NowFunc
を用いて時刻を差し替えています。
func init() { tz, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } flextime.NowFunc(func() time.Time { return time.Now().In(tz) }) } func main() { now := flextime.Now() fmt.Printf("now: %v\n", now.Format(time.RFC3339)) // now: 2009-11-11T08:00:00+09:00 }
https://play.golang.org/p/srJJReRbun_M
文字列から time.Time
に変換する方法
日付の文字列 2021-01-01
から time.Time
型に変換するには以下のAPIを使います。
time.Parse
time.ParseInLocation
time.Parse
はタイムゾーンが文字列に含まれていない場合、デフォルトでUTCに変換します。
func main() { d := "2021-01-30" nowDate, err := time.Parse("2006-01-02", d) if err != nil { panic(err) } fmt.Printf("nowDate: %v", nowDate.Format(time.RFC3339)) // nowDate: 2021-01-30T00:00:00Z }
変換時にタイムゾーンが重要な場合は time.ParseInLocation
で明示的にタイムゾーンを指定して文字列から変換しましょう。
func main() { d := "2021-01-30" tz, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } nowDateJST, err := time.ParseInLocation("2006-01-02", d, tz) if err != nil { panic(err) } fmt.Printf("nowDateJST: %v", nowDateJST.Format(time.RFC3339)) // nowDateJST: 2021-01-30T00:00:00+09:00 }
まとめ
*time.Location
のグローバル変数を更新してタイムゾーンを変換する方法は危険なのでやめましょう。現在時刻をJSTで取得する場合は In
を使って変換する方法が良いです。ラッパーをかますと、安全にかつ漏れが少なく変換することができます。
-
Go1.15からビルド時に
-tags timetzdata
を付与してビルドすることで、タイムゾーン情報を埋め込むことができるようになりました。詳細は https://golang.org/pkg/time/tzdata/ に記載されています。↩
GoDownloaderを使ってCLIツールのインストーラを作る
Goで書いたCLIツールのバイナリ配布には GoReleaser が便利だよ、というのはよく知られていることです。GoReleaser
ではYAML形式の .goreleaser.yml
ファイルという設定ファイルを記述し、GitHub Actionsなどと連携してリリースを自動化することができます。
GoReleaser
を用いたバイナリの配布の話は
- goreleaserを使ってGoで書いたツールのバイナリをGithub Releasesで配布する - $shibayu36->blog;
- Go で書いた CLI ツールのリリースは GoReleaser と GitHub Actions で個人的には決まり | tellme.tokyo
などを参考にしてください。
GoDownloader
本記事では GoReleaser
で作成したバイナリのインストーラを作成する方法を紹介します。GoDownloader を使います。
GoDownloaderのインストール
ローカルへのインストール方法は本来ですと go get github.com/goreleaser/godownloader
で取得できそうですが、2021/01/24現在は以下のように invalid pseudo-version
によってダウンロードできません。replace
ディレクティブを駆使することで対応は可能です。
$ go get github.com/goreleaser/godownloader go get: github.com/goreleaser/godownloader@none updating to github.com/goreleaser/godownloader@v0.1.0 requires github.com/goreleaser/goreleaser@v0.118.2 requires code.gitea.io/gitea@v1.10.0-dev.0.20190711052757-a0820e09fbf7 requires github.com/go-macaron/cors@v0.0.0-20190309005821-6fd6a9bfe14e9: invalid pseudo-version: revision is longer than canonical (6fd6a9bfe14e)
以下の2つのどちらかの方法が良いでしょう。
- 1.Releasesからv0.1.0のバイナリを取得する
$ curl -OL https://github.com/goreleaser/godownloader/releases/download/v0.1.0/godownloader_0.1.0_Linux_x86_64.tar.gz $ sudo tar -zxvf godownloader_0.1.0_Linux_x86_64.tar.gz -C /usr/local/bin
- 2.
git clone
してソースからビルドする
$ git clone --quiet --depth 1 https://github.com/goreleaser/godownloader && cd godownloader
Makefile
に以下のようにタスクを書くと便利です。
repo_name := d-tsuji/qiisync current_dir := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) installer: sh -c '\ tmpdir=$$(mktemp -d); \ cd $$tmpdir; \ git clone --quiet --depth 1 https://github.com/goreleaser/godownloader && cd godownloader; \ go run . -f -r ${repo_name} -o ${current_dir}/install.sh; \ rm -rf $$tmpdir'
使い方
とても簡単です。GitHubに .goreleaser.yml
がコミットされている状態で以下のようにコマンドを実行するだけです。
$ godownloader -f -r d-tsuji/qiisync -o install.sh • reading repo "d-tsuji/qiisync" on github • reading https://raw.githubusercontent.com/d-tsuji/qiisync/master/goreleaser.yml ⨯ reading https://raw.githubusercontent.com/d-tsuji/qiisync/master/goreleaser.yml returned 404 Not Found • reading https://raw.githubusercontent.com/d-tsuji/qiisync/master/.goreleaser.yml • setting defaults for loading environment variables • setting defaults for snapshoting • setting defaults for GitHub/GitLab/Gitea Releases • setting defaults for project name • setting defaults for building binaries • setting defaults for archives • setting defaults for Linux packages with nfpm • setting defaults for Snapcraft Packages • setting defaults for calculating checksums • setting defaults for signing artifacts • setting defaults for Docker images • setting defaults for Artifactory • setting defaults for S3 • setting defaults for Blob • setting defaults for homebrew tap formula • setting defaults for scoop manifest
上記のコマンドで指定しているオプションは以下のとおりです。
オプション | 説明 | デフォルト |
---|---|---|
-f |
出力されるファイルを上書きするか | false |
-r |
GitHubのリポジトリ名 | - |
-o |
出力されるファイル名 | 標準出力 |
実行すると install.sh
が作成されます。
例えば以下のような .goreleaser.yml
がコミットされている場合は次のような install.sh
が生成されます。
.goreleaser.yml
project_name: qiisync before: hooks: - go mod tidy builds: - goos: - linux - darwin - windows goarch: - amd64 - 386 main: ./cmd/qiisync binary: qiisync ldflags: - -s -w - "-X main.version={{.Version}}" archives: - format: zip name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" files: - LICENSE - README.md nfpms: - formats: - deb - rpm vendor: "d-tsuji" homepage: "https://github.com/d-tsuji/qiisync" maintainer: "Tsuji Daishiro" description: "Qiita CLI tool, support push and pull from/to local filesystem and Qiita." license: "MIT" file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}" replacements: amd64: 64-bit 386: 32-bit darwin: macOS linux: Tux brews: - description: "Qiita CLI tool, support push and pull from/to local filesystem and Qiita." github: owner: d-tsuji name: homebrew-qiisync commit_author: name: goreleaserbot email: goreleaser@carlosbecker.com homepage: "https://github.com/d-tsuji/qiisync" install: | bin.install "qiisync" test: | system "#{bin}/qiisync" checksum: name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
install.sh
#!/bin/sh set -e # Code generated by godownloader on 2021-01-23T14:08:53Z. DO NOT EDIT. # usage() { this=$1 cat <<EOF $this: download go binaries for d-tsuji/qiisync Usage: $this [-b] bindir [-d] [tag] -b sets bindir or installation directory, Defaults to ./bin -d turns on debug logging [tag] is a tag from https://github.com/d-tsuji/qiisync/releases If tag is missing, then the latest will be used. Generated by godownloader https://github.com/goreleaser/godownloader EOF exit 2 } parse_args() { #BINDIR is ./bin unless set be ENV # over-ridden by flag below BINDIR=${BINDIR:-./bin} while getopts "b:dh?x" arg; do case "$arg" in b) BINDIR="$OPTARG" ;; d) log_set_priority 10 ;; h | \?) usage "$0" ;; x) set -x ;; esac done shift $((OPTIND - 1)) TAG=$1 } # this function wraps all the destructive operations # if a curl|bash cuts off the end of the script due to # network, either nothing will happen or will syntax error # out preventing half-done work execute() { tmpdir=$(mktemp -d) log_debug "downloading files into ${tmpdir}" http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}" http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}" hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}" srcdir="${tmpdir}" (cd "${tmpdir}" && untar "${TARBALL}") test ! -d "${BINDIR}" && install -d "${BINDIR}" for binexe in $BINARIES; do if [ "$OS" = "windows" ]; then binexe="${binexe}.exe" fi install "${srcdir}/${binexe}" "${BINDIR}/" log_info "installed ${BINDIR}/${binexe}" done rm -rf "${tmpdir}" } get_binaries() { case "$PLATFORM" in darwin/386) BINARIES="qiisync" ;; darwin/amd64) BINARIES="qiisync" ;; linux/386) BINARIES="qiisync" ;; linux/amd64) BINARIES="qiisync" ;; windows/386) BINARIES="qiisync" ;; windows/amd64) BINARIES="qiisync" ;; *) log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new" exit 1 ;; esac } tag_to_version() { if [ -z "${TAG}" ]; then log_info "checking GitHub for latest tag" else log_info "checking GitHub for tag '${TAG}'" fi REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true if test -z "$REALTAG"; then log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details" exit 1 fi # if version starts with 'v', remove it TAG="$REALTAG" VERSION=${TAG#v} } adjust_format() { # change format (tar.gz or zip) based on OS true } adjust_os() { # adjust archive name based on OS true } adjust_arch() { # adjust archive name based on ARCH true } cat /dev/null <<EOF ------------------------------------------------------------------------ https://github.com/client9/shlib - portable posix shell functions Public domain - http://unlicense.org https://github.com/client9/shlib/blob/master/LICENSE.md but credit (and pull requests) appreciated. ------------------------------------------------------------------------ EOF is_command() { command -v "$1" >/dev/null } echoerr() { echo "$@" 1>&2 } log_prefix() { echo "$0" } _logp=6 log_set_priority() { _logp="$1" } log_priority() { if test -z "$1"; then echo "$_logp" return fi [ "$1" -le "$_logp" ] } log_tag() { case $1 in 0) echo "emerg" ;; 1) echo "alert" ;; 2) echo "crit" ;; 3) echo "err" ;; 4) echo "warning" ;; 5) echo "notice" ;; 6) echo "info" ;; 7) echo "debug" ;; *) echo "$1" ;; esac } log_debug() { log_priority 7 || return 0 echoerr "$(log_prefix)" "$(log_tag 7)" "$@" } log_info() { log_priority 6 || return 0 echoerr "$(log_prefix)" "$(log_tag 6)" "$@" } log_err() { log_priority 3 || return 0 echoerr "$(log_prefix)" "$(log_tag 3)" "$@" } log_crit() { log_priority 2 || return 0 echoerr "$(log_prefix)" "$(log_tag 2)" "$@" } uname_os() { os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in cygwin_nt*) os="windows" ;; mingw*) os="windows" ;; msys_nt*) os="windows" ;; esac echo "$os" } uname_arch() { arch=$(uname -m) case $arch in x86_64) arch="amd64" ;; x86) arch="386" ;; i686) arch="386" ;; i386) arch="386" ;; aarch64) arch="arm64" ;; armv5*) arch="armv5" ;; armv6*) arch="armv6" ;; armv7*) arch="armv7" ;; esac echo ${arch} } uname_os_check() { os=$(uname_os) case "$os" in darwin) return 0 ;; dragonfly) return 0 ;; freebsd) return 0 ;; linux) return 0 ;; android) return 0 ;; nacl) return 0 ;; netbsd) return 0 ;; openbsd) return 0 ;; plan9) return 0 ;; solaris) return 0 ;; windows) return 0 ;; esac log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" return 1 } uname_arch_check() { arch=$(uname_arch) case "$arch" in 386) return 0 ;; amd64) return 0 ;; arm64) return 0 ;; armv5) return 0 ;; armv6) return 0 ;; armv7) return 0 ;; ppc64) return 0 ;; ppc64le) return 0 ;; mips) return 0 ;; mipsle) return 0 ;; mips64) return 0 ;; mips64le) return 0 ;; s390x) return 0 ;; amd64p32) return 0 ;; esac log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" return 1 } untar() { tarball=$1 case "${tarball}" in *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; *.tar) tar --no-same-owner -xf "${tarball}" ;; *.zip) unzip "${tarball}" ;; *) log_err "untar unknown archive format for ${tarball}" return 1 ;; esac } http_download_curl() { local_file=$1 source_url=$2 header=$3 if [ -z "$header" ]; then code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") else code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") fi if [ "$code" != "200" ]; then log_debug "http_download_curl received HTTP status $code" return 1 fi return 0 } http_download_wget() { local_file=$1 source_url=$2 header=$3 if [ -z "$header" ]; then wget -q -O "$local_file" "$source_url" else wget -q --header "$header" -O "$local_file" "$source_url" fi } http_download() { log_debug "http_download $2" if is_command curl; then http_download_curl "$@" return elif is_command wget; then http_download_wget "$@" return fi log_crit "http_download unable to find wget or curl" return 1 } http_copy() { tmp=$(mktemp) http_download "${tmp}" "$1" "$2" || return 1 body=$(cat "$tmp") rm -f "${tmp}" echo "$body" } github_release() { owner_repo=$1 version=$2 test -z "$version" && version="latest" giturl="https://github.com/${owner_repo}/releases/${version}" json=$(http_copy "$giturl" "Accept:application/json") test -z "$json" && return 1 version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') test -z "$version" && return 1 echo "$version" } hash_sha256() { TARGET=${1:-/dev/stdin} if is_command gsha256sum; then hash=$(gsha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command sha256sum; then hash=$(sha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command shasum; then hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command openssl; then hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f a else log_crit "hash_sha256 unable to find command to compute sha-256 hash" return 1 fi } hash_sha256_verify() { TARGET=$1 checksums=$2 if [ -z "$checksums" ]; then log_err "hash_sha256_verify checksum file not specified in arg2" return 1 fi BASENAME=${TARGET##*/} want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) if [ -z "$want" ]; then log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" return 1 fi got=$(hash_sha256 "$TARGET") if [ "$want" != "$got" ]; then log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" return 1 fi } cat /dev/null <<EOF ------------------------------------------------------------------------ End of functions from https://github.com/client9/shlib ------------------------------------------------------------------------ EOF PROJECT_NAME="qiisync" OWNER=d-tsuji REPO="qiisync" BINARY=qiisync FORMAT=zip OS=$(uname_os) ARCH=$(uname_arch) PREFIX="$OWNER/$REPO" # use in logging routines log_prefix() { echo "$PREFIX" } PLATFORM="${OS}/${ARCH}" GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download uname_os_check "$OS" uname_arch_check "$ARCH" parse_args "$@" get_binaries tag_to_version adjust_format adjust_os adjust_arch log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}" NAME=${BINARY}_${VERSION}_${OS}_${ARCH} TARBALL=${NAME}.${FORMAT} TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL} CHECKSUM=${PROJECT_NAME}_${VERSION}_checksums.txt CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM} execute
長いですがちゃんと動作します。
CLIツールを使うユーザがバイナリをインストールするときは、以下のように実行します。
$ curl -sfL https://raw.githubusercontent.com/d-tsuji/qiisync/master/install.sh | sudo sh -s -- -b /usr/local/bin [sudo] password for tsuji: d-tsuji/qiisync info checking GitHub for latest tag d-tsuji/qiisync info found version: 0.0.5 for v0.0.5/linux/amd64 Archive: qiisync_0.0.5_linux_amd64.zip inflating: LICENSE inflating: README.md inflating: qiisync d-tsuji/qiisync info installed /usr/local/bin/qiisync
このコマンド一発でインストールできるので、とても便利です。一点だけ#161 にあるようにfeatureの開発は止まっている点は注意が必要です*1。ただし v0.1.0
のリリースでも十分便利に扱うことができます。
まとめ
GoReleaser
を使ってGitHubにバイナリを配布している場合は GoDownloader
で簡単にインストーラを作れて便利です!
*1:機能のメンテナンスは最善を尽くされるとのことです
DynamoDBを使ったデータ設計の反省点2020
2020年に取り組んだ案件の一つですが、サーバレスで検索アプリを開発していました。インフラはAWSを採用していたため、ほぼ必然的な運びでデータストアはDynamoDBを採用しています。業務アプリケーションではデータ設計が重要で、データ設計の良し悪しでアプリケーションの実装がシンプルor複雑になります。本記事はDynamoDBのデータ設計の反省点とこうすればよかったな、という振り返り記事です。
アプリケーション概要
映画館のシートような縦×横の空間に対して、人を割り当てるためのアプリケーションです(設定はダミーですが本質的には似ている構造です)。
制約
- 席の空間は予め決まっている
- 席は前(最前列)から詰めて座っていく
- 各席には座れる人の区分がきまっている(ジュニア/成人/シニア)
- 各席は事前に予約することもできるし、映画館の現地で予約なしに直接席を決めることもできる
- 事前に予約した場合も、最終的には現地で席を確定させる
- 30分前までには映画館内で着席していないといけない。そうでない場合は確定した席は取り消しされて、予約可能な席になる。別の方が確保することが可能
席の状態遷移図
反省点
反省点1:状態を区別するためにアイテムを分離させたこと
事前「予約」と、「確定」/「着席」の状態を区別するために2つのアイテムに分けたこと。これが一番の反省点です。なぜこのアイテム設計にしたか、というと、実は業務アプリを開発する前に、同じチームの別のメンバがPoCを実施していました。私が参画したときにはPoCである程度良好なフィードバックが得られていました。そのときに採用していたデータモデル上、事前「予約」と「確定」/「着席」の状態を別に管理していたため、この分離するモデルを採用しました。
分離したときのデータモデルのキーは以下のようなものです。
- ハッシュキー(
hkey
)- 日付をもつ
- ソートキー(
skey
)- 席の座標や状態を持つ
例えば日付が 20210101
で座標が縦=A、横=1の席で「空」の場合は以下の2つのアイテムを持ちます。ソートキーは複数の条件で絞り込みできるように複合キーになっています。
hkey |
skey |
... (その他の属性) |
---|---|---|
20210101 | YOYAKU#UNUSE#A#1 | ... |
20210101 | KAKUTEI#UNUSE#A#1 | ... |
- 日付が
20210101
で座標が縦=B、横=2の席で「確定」の場合
hkey |
skey |
... (その他の属性) |
---|---|---|
20210101 | YOYAKU#USE#B#2 | ... |
20210101 | KAKUTEI#USE#B#2 | ... |
まずRDBやDynamoDBといったKey-Valueストアに関わらず、 基本的な原則として状態をもつアイテム(レコード)は1つにするべき です。状態が2つ以上のアイテムに分かれていると管理が大変になって、アプリケーションが複雑になります。PoCではfeatureの機能を開発、動作させてフィードバックをもらうことが主眼ですが、プロダクションに採用するアプリ開発となると別です。保守運用しやすい、追加の開発コストが低いといった、単に機能だけでなく、別の重要な観点があります。データモデルはフラットに検討して、PoCの結果に引きづられるべきではありませんでした。
弊害
この分離モデルを採用したことの一番の弊害は、状態の把握が複雑になることです。データストアで把握すべき状態が事実上2倍になっています。特にある席を予約していたが、確定時は別の席を選択した場合は、もとの「予約」の席を未使用にする。確定として選択した席を「確定」にするだけでなく、別の人からの予約の対象にならないように、「確定」した席に対して「予約」の割当を行う必要があります。本来は不要な複雑性を持ち込んでいて、アプリケーションが必要以上に複雑になりました。保守性が悪く、これは大きな反省点です。
こうすべき
状態は1つの属性(キー)にまとめるべきです。今回の場合は、状態を「空、予約、確定、着席」という状態とすれば、必要十分でした。
hkey |
skey |
... (その他の属性) |
---|---|---|
20210101 | CYAKUSEKI#A#1 | ... |
20210101 | KAKUTEI#A#2 | ... |
20210101 | KAKUTEI#B#1 | ... |
20210101 | YOYAKU#B#2 | ... |
20210101 | EMPTY#C#1 | ... |
ここがつらい
つらみ1:ソートキーの複合キー
DynamoDBはRDBとはデータモデルが異なります。柔軟な検索クエリを投げることができないため、RDB以上にデータモデルが重要になります。RDBであれば、検索条件の追加、別テーブルのJOINなどSQLを用いて柔軟に検索できますが、DynamoDBではクエリはソートキーに対してのみ実施できます。その他の属性はデータの絞り込みには使えません。フィルターはデータを取得したあとに、結果を落とします。
ただし、座標をキー(ハッシュキーorソートキー)に含めないとアイテムが一意にならないため、座標をキーに含める設計は必須です。かつ状態(空、予約、確定、着席)によるクエリも必須ですので、状態もキーの一部に含める必要があります。
結果として以下のようなソートキーとなりました。DynamoDBで複雑なクエリを処理しようとすると、どうしてもソートキーが複雑になってしまい、アプリケーションのコードが複雑になるのがネックですが仕方がありません。
つらみ2:トランザクション管理
DynamoDBを使う上での制約ですが、トランザクションが最大25アイテムまで、という制約がつらいです。アプリケーションとしては数百件のレコードをトランザクショナルに処理したいのですが、DynamoDBでは実現できません。結局現在のところ、アプリケーション側でバッチインサートを行って、バッチインサート中にエラーが発生した場合はロールバックする、という処理をアプリケーションのコードで実装しています。ただしロールバック中にエラーが発生する可能性があり、この場合は運用でカバーする必要があるなど運用が複雑になってしまいました。
つらみ3:ソートキーの更新
他のつらみに比べるとだいぶ細かいで小さなことですが、ソートキーは更新できないので、アイテムを一度削除して、更新後のアイテムを追加する、という操作がDynamoDBでは必要です。まぁこの程度であれば、アプリケーション側でラッパーでも挟めばいいので、そこまでつらみではないです。
つらみへの対処
結局のところ、複雑なクエリやトランザクションをDynamoDBだけで処理しようとすると、どうしても複雑性がアプリケーションに漏れ出してしまう、と感じています。冒頭ではサーバレス環境ではほぼ必然的にデータストアはDynamoDB一択と言いましたが、2021年1月現在では、RDS Proxy+Amazon Aurora( Serverless)などの選択肢も十分考えられると思います。複雑性をインフラ側にも押し込むかアプリで吸収するのか、インフラ側に一部を寄せたときの可用性、運用保守性はどうか?など考慮するポイントは多いですが、設計しがいがあるポイントです。
まとめ
2020年の振り返りと2021年に取り組みたいこと
業務の振り返り
細かいところは公開できないので簡単に。
1月、小さい案件だけど顧客に近い立場で技術をリード。インフラ~バックエンドまで見れるスキルは役に立った。
2-4月、別チームのヘルプ、アーキテクチャは固まっていて、個別の機能開発、API開発やインフラの改善タスクがメインだった。素早く細かく対応して、チームに貢献はできたが、技術的な難易度は高くないので個人的にはまぁまぁの満足度。
5-12月、技術を中心にサーバレスで難易度が高い機能の設計、開発をやらせてもらった。サーバレスかつ技術的な難易度が高い案件でアーキ的なポジションをやらせてもらったのはとても良かった。個人のスキルで切り開いていく点も楽しいし、チームでレバレッジきかせることもできるのも楽しい。レビューだったりOJTだったり、チームメンバの育成に貢献できたのも良かった。
一方で一時期はまっていた競技プログラミングのコンテストには2021年は全く参加しなくなってしまった。水色になるまでに頑張って得た知識とか考え方は意味があったので良かったが、競技プログラミングのレートにこだわって頑張ることの価値は今の所薄いので、今後もコンテストに参加することはない気がする。
ソフトウェアエンジニア活動
Go言語を中心に、外部登壇や技術ブログ執筆など多くのアウトプットを残せた。とても良かった。会社の明示的な仕事としての外部登壇も複数あり(割愛)。
登壇(個人)
- インライン展開をGoでのぞいてみる, golang.tokyo #28, 2019年12月.
- ワークフローエンジンをGoで作る, Umeda.go 2020 Winter, 2020年01月.
- ワークフローエンジンをGoで作る (再演), golang.tokyo #29, 2020年02月.
- Go1.14のcontextは何が変わるのか, Go 1.14 Release Party in Japan, 2020年02月.
- Introduction to JSON handling in Go, umeda.go, 2020年11月.
技術記事
会社の技術ブログにいろいろ書いた。Qiitaとかはてなブログにもいろいろ書いている。会社以外の記事の出し元は統一したのがいいのかもしれないが
- Goの標準ライブラリのコードリーディングのすすめ, Future Tech Blog, 2020年03月.
- Go Tips連載3: ファイルを扱うちょっとしたスクリプトをGoで書くときのTips5選, Future Tech Blog, 2020年05月.
- 春の入門祭り 🌸 #01 Goのテストに入門してみよう!, Future Tech Blog, 2020年06月.
- DBスキーマを駆動にした開発のためのライブラリ調査, Future Tech Blog, 2020年07月.
- GoとSuffixArray , Future Tech Blog, 2020年08月.
- LambdaとGoを使ったサーバーレスWebAPI開発実践入門, Future Tech Blog, 2020年09月.
- GoでLambdaからLambdaを呼び出すときに気をつけたいポイント6選, Future Tech Blog, 2020年11月.
Qiita
Qiitaにもいろいろ書いた。「Go1.14のcontextは何が変わるのか」の記事はGoのカンファレンスの方に目をつけていただいて、登壇にもつながったので良かった。記事の質の濃淡問わず、引き続き今後も積極的に外部発信していく。
- Goでワーカープールを15分で実装する方法, 2020年1月.
- Go1.14のcontextは何が変わるのか, 2020年2月.
- 改行コードって難しいっ, 2020年4月.
- QiitaにCLIで投稿や更新できるツールを作った, 2020年4月.
- 【初中級編】Go言語を始める方のための落とし穴、問題の解決方法やよくある間違い, 2020年10月.
- Go言語を使ったTCPクライアントの作り方, 2020年12月.
- AWSでのMFAをちょっと便利に扱いたい, 2020年12月.
OSS
Goでいろいろなプロダクトを作れた。自分がやりたいことを実装するのにGoが一番書きやすいので、しばらくは今後もGoを書いていくと思う。まだまだ技術力不足で到達できなかった点もあるので(d-tsuji/clipboard
とか)、引き続き低レイヤーの知識も磨いていきたい。
- github.com/d-tsuji/clipboard
- github.com/d-tsuji/awsmfa
- github.com/d-tsuji/ttycopy
- github.com/d-tsuji/qiisync
- github.com/d-tsuji/gosdlisp
- github.com/d-tsuji/awesome-go-orms
- github.com/d-tsuji/flower
- github.com/future-architect/go-mcprotocol
- ...
OSSにStarをたくさんつけてもらえたのも良かった。ちなみにFindyの偏差値だと90以上あるんだけど、偏差値90とは一体なんだろう。相対的に高いのかもしれないが。
他人のプロダクトにもPRも色々出したが、機能開発とかバグFixというよりもTypoとかREADMEの指摘が多かった気がする。ただ外部のプロダクトで仕事に直結しないプロダクトの機能開発PRを出すのはなかなか優先度が上がらないので難しそう。future-architect/go-mcprotocol
は会社の案件で使用しているプロダクトで、コネクションプーリング周りの機能を開発した。仕事で使うプロダクトだとPRは出しやすい。
2021年に取り組みたいこと
技術的にはアーキテクチャ選定を含む設計力を磨いていきたい。設計でしくると、実装が複雑になったり運用コストが高くなる。実装レベルのリファクタリングはテストをしっかりと書いていれば素早く対応できるが、設計レベルで失敗があると、取り返すのが難しい。大抵の場合はリファクタリングのコストがかかりすぎるので、リファクタリングできないことが多い。YAGNIの原則はもちろんそうなんだけど、設計を後回しにする、ということではない。会社の仲間から学べることも多いし、ビジネスとより向き合わないといけないし、技術書を読んでいろいろ思考していきたい。Design It!は再度読んでみるのと、Release It! 2ndを読む。
- https://qiita.com/ma91n/items/207f32db1b51754d6933
- https://qiita.com/tmknom/items/be5c4b350f561991f2f5
- https://www.slideshare.net/yusuke/qcon-tokyo-2016
具体的には上記の一つは会社の先輩が書いた記事だが、こういう記事がかけるように観点を養っていくのと、プロジェクトで実践していく。思考&実践、フィードバックのイテレーションを回していきたい。
技術記事やOSSのアウトプット、外部登壇は2020年は豊作で良かった。2021年も引き続きアウトプットしていく。
技術を中心としたロールは変わらないけど、否応なく仕事では少なくともプロジェクトマネージメント的なロールも求められそうなので、少しずつ勉強していく。プロジェクトマネージメントも立派なスキル(暗黙的にできていることもあるだろうけど)。
その他
健康第一。30代になって、体力的にも無理がきかないので、メリハリをつけて頑張っていきたい。特に積極的に休むことが大事。また、リモートワーク中心の生活になったので、ランニングとか散歩とか積極的に取り組んだ。これは良かったと思うので2021年も継続。ちなみにコンピュータサイエンスの修士号も頭の片隅にあって、JAISTの研究室とかながめていたけど、今の所あまりいきたい研究室がなかったのでひとまず2021年も後回し。
あとは囲碁クエスト(9路)でレート1900を目指したい。「決定版! 囲碁 9路盤完全ガイド」を読んで筋を勉強するのと、囲碁クエストで負けた棋譜をAIで解析してベターな手を検討する。
1バイトのファイルを作成する
何をしたくて書いたもの?
同僚から1バイトのファイルを作成したいんだけど、なぜか2バイトになってしまう。原因がわからないと聞かれました。OSはLinuxです。
例えば以下のようなファイルを考えてみます。
tsuji@DESKTOP-DCISGC3:/tmp/gomi$ ls -l total 0 -rw-r--r-- 1 tsuji tsuji 2 Dec 27 20:39 hoge_1b.txt tsuji@DESKTOP-DCISGC3:/tmp/gomi$ cat hoge_1b.txt a
たしかに a
の1文字しかファイルに書き込んでいないのですが、ls -l
コマンドで確認すると2バイトと表示されています。
結論から言うと、これは改行コードによって1バイト消費しているためです。od
コマンドを利用すると、以下のように調べることができます。
tsuji@DESKTOP-DCISGC3:/tmp/gomi$ od -tx1 hoge_1b.txt 0000000 61 0a 0000002
od
はファイルを8進数や16進数でダンプするコマンドです。オプションがいろいろあり -tx1
とするとファイルの内容を1バイト単位で16進数で表示することができます。ファイルの中身を表示すると a
(16進数で 61
)の他に 0a
が表示されています。これは改行コードのLFを意味します。
vim
で操作する場合はデフォルトだと改行コードが付与されるので
tsuji@DESKTOP-DCISGC3:/tmp/gomi$ vim hoge_1b.txt :set bin noeol :w tsuji@DESKTOP-DCISGC3:/tmp/gomi$ ls -l hoge_1b.txt -rw-r--r-- 1 tsuji tsuji 1 Dec 27 20:48 hoge_1b.txt
などとして改行コードを追加しないようにする、などといった方法で改行コードをつけずにファイルを作成するとうまく動作します。もちろん echo -n
などといった方法もあります。
tsuji@DESKTOP-DCISGC3:/tmp/gomi$ echo -n a > hoge_1b.txt tsuji@DESKTOP-DCISGC3:/tmp/gomi$ ls -l hoge_1b.txt -rw-r--r-- 1 tsuji tsuji 1 Dec 27 20:49 hoge_1b.txt