Unengineered Weblog

PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND

Go で型変換を含んだイテレータの処理をメソッドチェーンっぽく記述する

この記事は「はてなエンジニア Advent Calendar 2024 - Hatena Developer Blog」の44日目の記事です。

JavaScriptScala などで配列やリストを処理するときメソッドチェーンを使って書くことが多い。例えば次のコードは JavaScript のコードである。

type thing = {
  count: number;
  name: string;
};

const somethings: thing[] = ...;

somethings
  .filter(x => x.count > 10)
  .sort((a, b) => a.count - b.count)
  .map(x => x.name)

Method chaining - Wikipedia から引用して加筆。

このコードにおいて map が行う型の変換に注目してほしい。 thing 型から string 型に変わっている。

Go はこのようなメソッドチェーンによる型を変換ができない。なぜなら現状 Go は型パラメータをもったメソッドを定義できないからだ。例えば JavaScriptmap と同等なものを仮に Go で書けるのであれば、次のように書きたい。

// 注意: 以下のコードは実際の Go では動かない!

// Stream[A] はイテレータをメソッドチェーンで処理できるようにメソッドを提供する。
type Stream[A any] ...

func (s *Stream[A]) Map[B any](f func(A) B) *Stream[B] {
    ...
}

しかし残念なことに現状の Go では、"[B any]" のように型パラメータをもったメソッドを定義することができないので上のコードは構文エラーである。

ところで Go の関数には型パラメータをつけることができる。私は関数によってメソッドチェーンっぽい記述ができることに気がついた。

最初に結論をだそう。引用した JavaScript のメソッドチェーンは、Go では次のように書ける。

type thing struct {
    count int
    name string
}

var somethings iter.Seq[thing] = ...

result := Stream(somethings,
    Filter(func(x thing) bool { return x.count > 10 },
        Sort(func(a thing, b thing) int { return a.count - b.count },
            Map(func(x thing) string { return x.name },
                End))))
// result の型: iter.Seq[string]

次のページで実際に実行できる。

go.dev

なにやらお尻に括弧が多いけど、いかにもメソッドチェーンっぽいではないか(!?)。Stream はメソッドチェーン(のようなもの)を開始する関数だ。 JavaScript ではメソッドであった .filter.sort, .map が Go では関数としてそれぞれ Filter, Sort, Map になっている。以下 Map などを変形関数と呼ぶことにする。変形関数は後続の処理をメソッドでつなげる代わりに第2引数で受け取っている。チェーンの最後にある Map の第二引数の End はメソッドチェーンを終わらせる関数である。重要な点として型は推論されるから、型パラメータをわざわざ記述する必要がない。

これはどういう仕組みだろうか。Stream, End, Sort, Filter, Map の定義は次のようになる。

// stream を開始する。
func Stream[F, A any](seqA iter.Seq[A], cont func(iter.Seq[A]) F) F

// stream を終了する。
func End[F any](f F) F

// stream において要素を sort する関数
func Sort[F, A any](cmp func(A, A) int, cont func(iter.Seq[A] F) func(iter.Seq[A]) F

// stream において fn(a) == false の値 a を除去する関数。
func Filter[F, A any](fn func(A) bool, cont func (iter.Seq[A]) F) func(iter.Seq[A]) F

// stream において A -> B にする関数。
func Map[F, A, B any](fn func (A) B, cont func (iter.Seq[B]) F) func(iter.Seq[A]) F

Sort, Filter や Map はどれも似た引数を持っている。例えば Map について考えよう。Map の第1引数 fn は説明不要だろう。しかし第2引数の cont は一体何?型 F って誰? 奇妙なことに引数に変換前の型 B が登場し、返り値に変換前の型 A が登場している。

第2引数 cont はどこから来るのかを念頭におくと考えやすい。 さきほど「変形関数は後続の処理をメソッドでつなげる代わりに第2引数で受け取っている」とさらっと書いた。 この後続こそが cont なのである。 上の例で FilterSort(..., Map(..., End)) という後続の処理を第二引数で受け取っている。 同様に SortMap(..., End) という後続を受け取っている。 つまり cont は「後続の処理をする」関数である。 ちなみに cont の引数は後続で初めに処理される値である。 上の例なら Filter が受け取った後続は最初の処理 である Sortを する値を受け取る。

cont の返り値の型 F は後続の最終結果である。 上記の例で Map は後続として変形関数の返り値ではなく、 End を受け取っている。End のコードは次の通り。

func End[F any](f F) F {
    return f
}

上の例では Map の後続は何もないEnd は単に f を受け取って何もせずに f を返す関数である。

さて変形関数の返り値は直前の変形関数に渡す継続である。 例えば、上記の Sort の返り値は Filter の第二引数に渡されている。すでに書いたとおり Filter の第二引数は 「Sort(..., Map(..., End)) という後続の処理」であるから、つまり Sort は「Sort(..., Map(..., End)) という後続の処理」を作って返しているのである。Sort の実装は次の通り

func Sort[F, A any](cmp func(A, A) int, cont func(iter.Seq[A]) F) func(iter.Seq[A]) F {
    return func(seqA iter.Seq[A]) F {
        newSeqA := slices.Values(slices.SortedFunc(seqA, cmp))
        return cont(newSeqA)
    }
}

チェーンの始まりにおく、継続を最後に受け取る Stream は次の通り。

func Stream[F, A any](seqA iter.Seq[A], cont func(iter.Seq[A]) F) F {
    return cont(seqA)
}

何度も述べたが、 cont は後続の処理をする関数である。 Stream ではただ単に後続の処理を呼ぶだけだ。

このように継続を渡していくプログラミング技法は「継続渡しスタイル」と言われている。

ja.wikipedia.org

この継続を使ってメソッドチェーンを再現する方法は拡張ができそうだ。 例えば

  • End の代わりにReduce や合計を計算する Sumイテレータを配列にする Collect を定義することができるだろう。
  • Map と同じように FlatMap を定義することもできるだろう。

OpenTelemetry Collector をカスタムビルドするとき ocb (builder) のバージョンを固定する

注意: このブログは私のプラクティスを記述したもので、 OpenTelemetry 公式の方法を紹介したものではありません。

OpenTelemetry Collector をプロダクションにデプロイする時、自分がつかうコンポーネントのみを含めた Collector をカスタムビルドすることが推奨されています。 カスタムビルドは ocb (OpenTelemetry Collector Builder) を使っておこないます。このブログでは ocb のバージョンを固定する方法を紹介します。 ocb を使った Collector の作り方自体は他のブログに譲ります。

opentelemetry.io

結論を先に言うと tools.go を用いた方法です。 Go に詳しい方ならすぐわかると思います。次のページが詳しいです。

github.com

簡単に言うと Go のモジュールマネージャに ocb のバージョンを固定させます。 Go のモジュールマネージャは次のチュートリアルが詳しいです。このブログでは実行するコマンドを書くだけに留め、詳しい説明は次のブログを参照してください。

go.dev

また(このブログを書いたときの最新バージョンは Go 1.23)将来のバージョン Go 1.24 からは tools.go ではなく、もっと簡潔な方法できるようになりそうです。

zenn.dev

前提

Git などのバージョン管理ツールを使って OpenTelemetry Collector builder の設定ファイルやバージョンを管理しようとしている。

以下の説明は Git で管理しているディレクトリ mycustomcollector/ の中で実行しているとする。 また mycustomcollector/ には OpenTelemetry Collector Builder の設定ファイルがあるとする。

$ pwd
/path/to/mycustomcollector

$ ls
builder-config.yaml # OpenTelemetry Collector Builder の設定ファイル

go mod init

Go のモジュールを初期化しましょう。次のコマンドを実行します。

go mod init {このディレクトリをおくリポジトリのURL}{/ディレクトリ...}

{} 内は適宜値を入れてください。お試しで作る場合、つまりリポジトリにおかない場合、{} 内は何でもいいです。Go のモジュールを定義するファイル go.mod を生成します。 go.mod にはモジュールが依存するサードパーティモジュールも記述します。後々の説明で依存モジュールに ocb を含めます。これによって Go に ocb のバージョンを管理させるのです。

この説明では以下、

go mod init example.com/mycustomcollector

を実行したとします。

tools.go を書く

次のようなファイルを作ります。tools.go という名前にします。

//go:build tools

package tools

import _ "go.opentelemetry.io/collector/cmd/builder"

tools.go は普通の Go プログラムのファイルです。tools.go というファイルを Go が特別扱いすると言うわけではありません。名前も tools.go でなくても良いですが、習慣的に tools.go となっています。

先頭の行の //go:build は Build Constraints と呼ばれるもので、コードをビルドするときにこのファイルを含めるかの条件を書くものです。例えば Unix-like な環境だけビルドに含めたいファイルは //go:build unix と書きます。Go は今回の条件である tools を (unixwindows などと違って) 知りません。この場合、常にビルドに含まれないファイルとなります。

"go.opentelemetry.io/collector/cmd/builder" をインポートするだけのファイルです。go.opentelemetry.io/collector/cmd/builder は ocbのパスですね。通常の Go のコードではライブラリをインポートしますが、ここではコマンド (ocb) をインポートします。これによって go.mod に記述された依存モジュールに ocb を追加させるのです。

import"go.opentelemetry.io/..." の間にある _ を忘れないようにしてください。ブランクインポートと言うものです。詳しい説明は省きます。

go mod tidy を実行する

go mod tidy はgo.mod を整頓するコマンドです。モジュール以下にある Go のコードファイルを探し、go.mod に記述していない依存サードパーティパッケージを go.mod に追加したり、 go.mod にあるのに誰も依存していないサードパーティパッケージを消したりします。今回は tools.go が依存しているパッケージである ocb を go.mod に追加します。

go mod tidy

すると go.mod は次のようになります。

module example.com/mycustomcollector

go 1.23.3

require go.opentelemetry.io/collector/cmd/builder v0.116.0

require (
    github.com/fsnotify/fsnotify v1.7.0 // indirect
    github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
    github.com/inconshreveable/mousetrap v1.1.0 // indirect
    github.com/knadh/koanf/maps v0.1.1 // indirect
    github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect
    github.com/knadh/koanf/providers/env v1.0.0 // indirect
    github.com/knadh/koanf/providers/file v1.1.2 // indirect
    github.com/knadh/koanf/providers/fs v0.1.0 // indirect
    github.com/knadh/koanf/v2 v2.1.2 // indirect
    github.com/mitchellh/copystructure v1.2.0 // indirect
    github.com/mitchellh/reflectwalk v1.0.2 // indirect
    github.com/spf13/cobra v1.8.1 // indirect
    github.com/spf13/pflag v1.0.5 // indirect
    go.uber.org/multierr v1.11.0 // indirect
    go.uber.org/zap v1.27.0 // indirect
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sys v0.21.0 // indirect
    gopkg.in/yaml.v3 v3.0.1 // indirect
)

またパッケージのハッシュが記録された go.sum も生成されます。

generate.go ファイルを作る

この準備が終わると、 go.mod があるディレクトリ以下で

go run go.opentelemetry.io/collector/cmd/builder

と実行すると、常にバージョンが固定された ocb が実行されます。上の go.mod ならば常に v0.116.0 の ocb が実行されることになります。

毎回 go run go.opentelemetry.io/collector/cmd/builder を実行してもよいですが、長いし、めんどくさいので go generate コマンドで実行できるようにしましょう。

次のような Go のプログラムのファイルを作ります。名前は何でもいいですが、generate.go にしました。たった2行です。

package mycustomcollector

//go:generate go run go.opentelemetry.io/collector/cmd/builder --config {builder の設定ファイル} --skip-compilation

{builder の設定ファイル} は今回の場合 builder-config.yaml になります。

go generate はモジュール以下にある go のソースコードから //go:generate {cmd ...} と書かれた行を探して cmd ... を実行するコマンドです。Go ではソースコードを自動生成することが多々あり、その用途に使われることが多いです。 ocb はカスタムコレクターのソースコードを生成するもので、この用途にぴったりですね。

go.dev

--skip-compilation はカスタムコレクターのソースコードを生成するだけで、コレクターのバイナリを作らないように ocb に指定するものです。私の場合、バイナリ自体は Docker でビルドするのでバイナリのビルドが不要です。

コレクターを生成する

ここまで準備をしたらあとは go generate を実行するだけ ocb が実行され、カスタムのコレクターのコードが生成されます。

go generate

ここまで実行するとカレントディレクトリは次のようになっているはずです。 collector/ は ocb が自動生成したコレクターのソースコードが入っているディレクトリです。 ocb の設定ファイル builder-config.yaml で ./collector に出力するように指定しました。

$ ls -F -1
collector/
builder-config.yaml
generate.go
go.mod
go.sum
tools.go

今後 builder-config.yaml を変更したとき

ocb の設定ファイル builder-config.yamlを変更したら、毎回 go generate をしましょう。バージョンが固定された ocb でコレクターをビルドしてくれます。

自動生成された collector/ 以下を git で管理するか?

Go では自動生成したファイルも git add するのが習慣になっています。その延長で私は ocb が自動生成した collector/ 以下も git で管理しています。ただこれは私がそうしているだけで OpenTelemetry 公式がそのように書いた文章があったわけではないです。

Span Metrics Connector で SQL のクエリの処理時間を計測する

この記事は Mackerel Advent Calendar 2024 の 13日目の記事です。

Web アプリケーションのパフォーマンスの指標としてレスポンスタイムあげられます。レスポンスタイムは SQL の処理時間がボトルネックになることも多いです。 Web アプリケーションの SQL の処理時間を計測する手法は多くあります。

このブログでは OpenTelemetry Collector の Connector として 提供されている Span Metrics Connector を活用し Mackerel で SQL のクエリごとの処理時間を計測し、さらに監視設定を設定する方法を説明します。

Span Metrics Connector とはトレースのスパンの情報から次のメトリックを計算する Connector です。

例えば次のようなトレースを計測したとしましょう。

Web Application のトレース

この例では POST /api/register のスパンが1個、db.Begin が1個、 db.Prepare が(表示では省略されているが)2個、 stmt.Exec が2個あります。Span Metrics Connector は計測された全てのトレースのスパンの数を通して足し合わせて call、そしてこれらのスパンの処理時間のヒストグラムduration のメトリックにします。

次のブログでは Span Metrics Connector についてはさらに詳しく解説されています。 zenn.dev

今回は Span Metrics Connector の実用的な例として SQL のクエリの処理時間を計測してみましょう。

このブログでは Web Application の例として ISUCON13 を使います。Go で記述されたサーバーを使います。コードの大半はそのままで Trace だけ導入します。

github.com

DB のクエリのトレースを取得する

まずはSQLの処理時間をトレースとして計測しましょう。 ISUCON13 は DB のライブラリとして jmoiron/sqlx を使っています。 jmoiron/sqlx に対応した OpenTelemetry 計装ライブラリ uptrace/opentelemetry-go-extra/otelsqlx があるので、これをアプリケーションに組み込みます。

go get github.com/uptrace/opentelemetry-go-extra/otelsqlx

差分としては次のようになります。パッケージのインポート文の差分を除くと一行ですね。

@@ -93,7 +100,7 @@ func connectDB(logger echo.Logger) (*sqlx.DB, error) {
                conf.ParseTime = parseTime
        }
 
-       db, err := sqlx.Open("mysql", conf.FormatDSN())
+       db, err := otelsqlx.Open("mysql", conf.FormatDSN())
        if err != nil {
                return nil, err
        }

さらに OpenTelemetry の Trace の exporter を設定します。これはボイラープレートコードのため省略します。トレースの Export 先は OpenTelemetry Collector です。

Span Metrics Connector の設定

次に Span Metrics Connector の設定をします。 Span Metrics Connector の設定だけ引用すると次のようになります。ブログの最後に設定全文を載せました。

  spanmetrics/exponential:
    histogram:
      exponential:
        max_size: 160
    dimensions:
      - name: db.statement
    aggregation_temporality: "AGGREGATION_TEMPORALITY_DELTA"
    metrics_flush_interval: 60s

注目する点として dimensionsdb.statement があります。 Span Metrics Connector が生成するメトリックはデフォルトの設定だと service.name, span.name, span.kind, status.code の属性しかありません。デフォルトの設定ではSQL の処理スパンからメトリックを生成してもクエリの情報は含めてくれません。今回はクエリごとの処理時間を計測したいですから dimensionsdb.statement を設定します。db.statement は実行した SQL のクエリを表す span の属性です。

SpanMetrics Connector は events という設定もあり、こちらでメトリックに db.statement を追加するようにしても良いかもしれません。私は未検証です。

さらにヒストグラムの種類として exponential ヒストグラムにしました。 exponential ヒストグラムについては僕のブログを参照ください。

blog.rmatsuoka.org

メトリックの送信先は Mackerel です。

さて実行してみましょう

ISUCON 13 のベンチマーカーを実行しました。

Mackerel のメトリックエクスプローラ

さて投稿したメトリックを Mackerel のメトリックエクスプローラで見てみましょう。ベンチマーカーは数分しか起動しないので、メトリックもそのぶんだけです。

callsduration のメトリックがあります。処理時間は duration のメトリックです。注目するメトリックは処理時間の最大値 duration.max や XX パーセントタイルである duration.XXp です。

SQL の処理時間のメトリックだけに限定する

今回は DB のスパンだけ計測したので、表示されるメトリックは DB のものだけです。通常のアプリケーションでは HTTPやカスタムなど DB 以外の span も計測していることがほとんどだと思います。SQL の処理時間のメトリックだけ見たい時は db.statment のラベルを持つメトリックだけを見えるようにしましょう。ラベルフィルターに db.statment = * と指定すればできます。

グラフの凡例を変更してみる

デフォルトの凡例は見にくいので変更してみましょう。 凡例設定で span.name, db.statment と設定しましょう。

凡例を設定する

設定した凡例

これによって SELECT * FROM livestream_tags WHERE livestream_id = ? というクエリの 90 パーセントタイルが 3.84ms であることがよくわかります。

遅いクエリがあったときにアラートを発報させる

クエリによる監視をつかって遅いアラートが発生した時にアラートを発報させるようにしてみましょう。 今回は時間がかかっているクエリを調べたいのでクエリの処理時間の最大値である duration.max を監視します。

右上の ... から Add monitor を選択

さらにちょっとした隠し機能ですが監視設定の名前にラベルにテンプレートを使って、属性を含めることができます。便利ですね。画像では監視設定の名前を {{span.name}} {{db.statement}} にしてみました。

名前にテンプレートを使用する

アラートが発報する次のように表示されます。テンプレートが評価されてアラートの名前が db.Prepare SELECT * FROM livestream_tags WHERE livestream_id = ? となっています。

最後に

Span Metrics Connector と Mackerel のメトリックエクスプローラを駆使して SQL のクエリ処理時間を計測する方法を解説しました。そこそこお手軽で計測できると思います。

おまけ Collector の config 全文

---
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
exporters:
  debug:
    verbosity: basic
  otlp/michizane:
    endpoint: otlp.mackerelio.com:4317
    compression: gzip
    headers:
      mackerel-api-key: "${env:MACKEREL_APIKEY}"

processors:
  batch:

connectors:
  spanmetrics/exponential:
    histogram:
      exponential:
        max_size: 160
    dimensions:
      - name: db.statement
    aggregation_temporality: "AGGREGATION_TEMPORALITY_DELTA"
    metrics_flush_interval: 60s

service:
  pipelines:
    traces:
      receivers:
        - otlp
      processors:
        - batch
      exporters:
        - debug
        - spanmetrics/exponential
    metrics:
      receivers:
        - spanmetrics/exponential
      processors:
        - batch
      exporters:
        - otlp/michizane
        - debug

さくらインターネットで VPS を立てて遊び、 ログを数える Mackerel プラグインを作った

私は Web アプリケーションエンジニアを自称しているが、実は VPS で遊んだことのない怪しい人間なので、土日で VPS で一通り遊んでみた。

サーバーを建てる

さくらの VPS の 1G プランを選んだ。 OS は Ubuntu 24.04 amd64

サーバーを立てたら SSH でパスワードログインを禁止したりと一般的な作業を行った。

さてこの作業をしているうちに私は初めて知ったのだが、 GitHub に登録している公開鍵のリストは次の URL にダウンロードできるみたいだ。

https://github.com/{username}.keys

次のようにすれば、普段使っている公開鍵をサーバーに保存できてしまう。とても便利だ。

$ curl https://github.com/{username}.keys >> ~/.ssh/authorized_keys 

Mackerel Agent をインストールする

はてなのエンジニアなので当然 Mackerel Agent をインストールした。

HTTP サーバーを建てる

今回は ngnix など使わず直接 Go で HTTP サーバーを建てた。以下 hello と名付ける。

github.com

リクエストをすると hello, {リクエスト元IP} を返すだけのサーバーである。 HTTPS で通信したかったので certbot を使って証明書を取得した。 次の公式ドキュメントとブログを参考にした。

eff-certbot.readthedocs.io

maku.blog

certbot で証明書を取得すると「今後自動的に証明書の更新をするよ!」とメッセージがでてくる。はてどうやって? crontab みてもそんなのなさそうだが…と思ったが、今は systemd-timer を使うらしい。

さくらのVPS はパケットフィルターというものがあり、はじめは 22 しか開放していない。HTTP サーバーを建てるため 443 を開ける必要がある。

ログを見て楽しむ

サーバーとしての作業はこれ以上特に何もやってない。

知らない人が SSH でログインを試行してくるログとかHTTP サーバーの変なパスへのリクエストのログを眺めて楽しんでいた。

たとえば次のようなパスへアクセスしてくるログを見ていたりした。

url=/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh
url="/index.php?lang=../../../../../../../../usr/local/lib/php/pearcmd&+config-create+/&/<?echo(md5(\"hi\"));?>+/tmp/index1.php

ログをメトリックにして楽しむ

たまに変なログが出てきて面白いが、ほとんどは面白くないログばかりだ。 変なログが来たときだけ見たい。ということでログの数を Mackerel へメトリックとして投稿することにした。 journalctl を定期的に叩いて特定のログの数える plugin が欲しかった。が、探してみても手頃なものがなかったので自作した。 名付けて mackerel-plugin-journal-count である。

github.com

これは systemd の service unit の名前と正規表現を引数にとり、その unit が吐き出したログのうち正規表現にマッチする数を metric として Mackerel に投稿するプラグインである。

例えば mackerel-agent.conf につぎのような設定をする。

[plugin.metrics.journal-count-ssh]
command = "/usr/local/bin/mackerel-plugin-journal-count -u ssh -g 'invalid user .* from'"

すると sshd のログのうち次の行の数をメトリックとして Mackerel に投稿してくれる。

Invalid user XXX from XXX.XXX.XXX.XXX port XXXX

次の画像は HTTP サーバー(hello)のリクエスト数 と sshd の invalid user のログの数のメトリックのグラフである。 HTTP サーバーのリクエスト数を INFO request にマッチするログの数、 sshd の invalid user のログイン試行回数を invalid user .* from にマッチするログの数としてメトリックにしている。sshd の invalid user を数える正規表現はこれよりももっと良さそうなのがありそうだ。

この mackerel-plugin-journal-count はシェルスクリプトで書いた(ほとんど awk が処理を担っているが)。私が真面目に向き合った最初の言語はシェルスクリプトなので、シェルスクリプトを書くのはやっぱり楽しいですね。

Unengineered Weblog のテーマを自作した

このブログのスクリーンショット

このブログのテーマを自作してみました。

このテーマはお堅い(?)ソフトウェア技術書から発想しました。「プログラミング言語C」とかちょっと前のオライリーの本とかをイメージしています。背景色に書籍の紙をイメージしたクリーム色、フォントは明朝体、ボーダーラインは少なめ、文章中の強調とセクションタイトルは太字のゴシック体といった感じです。 ちなみに書籍に使われているの淡いクリーム色の紙を淡クリームキンマリというそうです。初めて知りました。

シンタックスハイライト

コードのシンタックスハイライトを無くしてみました。技術書にはシンタックスハイライトがついていないことも多く、それを真似してみました。ただコメントには色をつけました。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    for i := 0; i < 10; i++ {
        dur := time.Duration(rand.Intn(1000)) * time.Millisecond
        fmt.Printf("Sleeping for %v\n", dur)
        // Sleep for a random duration between 0-1000ms
        time.Sleep(dur)
    }
    fmt.Println("Done!")
}

go.dev/play の 「Sleep」サンプルを引用。

シンタックスハイライトを無くしてもそんなに読みづらくはないんじゃないかなと思っています。以前 Go の公式サイトにシンタックスハイライトが無いことが話題になりました。これも意識しています。とはいえ僕はコーディングするときはシンタックスハイライトが使えるなら使っています。シンタックスハイライトがあるとクオートの閉じ忘れの発見が簡単ですね。

blog.stenyan.jp

ただ diff の出力だけは着色によって見やすさが大きく変わるので、色をつけました。

--- main.go  2024-06-17 18:00:56
+++ main_new.go   2024-06-17 18:01:22
@@ -8,7 +8,7 @@
 
 func main() {
    for i := 0; i < 10; i++ {
-       dur := time.Duration(rand.Intn(1000)) * time.Millisecond
+       dur := time.Duration(rand.Intn(1000)) * time.Second
        fmt.Printf("Sleeping for %v\n", dur)
-       // Sleep for a random duration between 0-1000ms
+       // Sleep for a random duration between 0-1000s
        time.Sleep(dur)

横に長いコードの表示

横に長いコードを表示した時の挙動を次のようにしてみました。

  • ブラウザのウィンドウの横幅が長いときは全部表示

このブログのページのスクリーンショット。ブラウザのウィンドウの幅が広い。

  • ブラウザのウィンドウの横幅が短いときは横スクロール表示

このブログのページのスクリーンショット。ブラウザのウィンドウの幅が狭い。

全部表示にするとサイトのレイアウトが崩れることもありますが、見やすさを優先しました。ユーザーがブラウザのウィンドウを横に広げたときは広く表示されてほしいだろうからです。

長いコード列のサンプルです。

func QueryScan(ctx context.Context, db *sql.DB, query string, args ...any) iter.Seq2[scanfunc, error]

つくりかた

このテーマは hatena/Hatena-Blog-Theme-Boilerplate から改変して作りました。GitHub で公開しています。

github.com

つかいかた

テーマストアで公開しています。どうぞご利用ください。

blog.hatena.ne.jp

curl でステータスコードだけを得る

ある URL からのレスポンスのステータスコードだけ知りたいときがある。404 かどうかだけわかればよいときだったり。

そういうときは curl--write-out / -w オプションが使える。このオプションには %{http_status} などの変数を含んだテンプレートを渡す。このオプションを指定して実行すると、レスポンスボディが出力したあとにテンプレートを展開した文字が出力される。

everything.curl.dev

たとえば、つぎのように使う。

curl -w 'status code: %{http_code}\n' example.com
<!doctype html>
<html>
... omitted
</html>
status code: 200

レスポンスボディのあとに -w に渡したテンプレート status code: %{http_code}\n が展開されたものが出力された。ステータスコードが 200 であることがわかった。

テンプレートに使える変数はいっぱいある。

everything.curl.dev

シェルスクリプトステータスコードを変数に入れるときは次のようにする。

code=$(curl -sSL -o /dev/null -w '%{http_code}' example.com)