ollamaはローカルでLLMを動かすためのツールです。モデルファイルは数GB〜数十GBあるため、ダウンロード進捗やディスク使用量を「1073741824 B」ではなく「1.0 GB」のように人間が読みやすい形で表示する必要があります。format/bytes.goはその変換だけを担う小さなファイルで、switch文と型変換という基本文法だけで書かれているため、実務コードの「型変換のクセ」や「早期returnの使い方」を学ぶのにちょうど良い教材です。同じファイルにはKiB単位版のHumanBytes2という関数もありますが、今回はKB/MB/GB版のHumanBytesに絞って解説します。

コード

package format

import (
	"fmt"
	"math"
)

const (
	Byte = 1

	KiloByte = Byte * 1000
	MegaByte = KiloByte * 1000
	GigaByte = MegaByte * 1000
	TeraByte = GigaByte * 1000

	KibiByte = Byte * 1024
	MebiByte = KibiByte * 1024
	GibiByte = MebiByte * 1024
)

func HumanBytes(b int64) string {
	var value float64
	var unit string

	switch {
	case b >= TeraByte:
		value = float64(b) / TeraByte
		unit = "TB"
	case b >= GigaByte:
		value = float64(b) / GigaByte
		unit = "GB"
	case b >= MegaByte:
		value = float64(b) / MegaByte
		unit = "MB"
	case b >= KiloByte:
		value = float64(b) / KiloByte
		unit = "KB"
	default:
		return fmt.Sprintf("%d B", b)
	}

	switch {
	case value >= 10:
		return fmt.Sprintf("%d %s", int(value), unit)
	case value != math.Trunc(value):
		return fmt.Sprintf("%.1f %s", value, unit)
	default:
		return fmt.Sprintf("%d %s", int(value), unit)
	}
}

引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。

上から順に読む

const (
	Byte = 1

	KiloByte = Byte * 1000
	MegaByte = KiloByte * 1000
	GigaByte = MegaByte * 1000
	TeraByte = GigaByte * 1000

const ( ... ) は複数の定数をまとめて宣言する構文です。Byteを1と定義し、KiloByteはByteの1000倍、MegaByteはKiloByteの1000倍…と、前の定数を使って次の定数を作っています。これらは型を明示していない「型なし定数」なので、後でint64にもfloat64にも自然に変換して使えます。SI単位(1000区切り)なのでKB=1000バイト、MB=1000000バイトという定義です。

KibiByte = Byte * 1024
	MebiByte = KibiByte * 1024
	GibiByte = MebiByte * 1024
)

こちらは2進接頭辞(1024区切り)の定数です。KB(1000)とKiB(1024)は本来別物ですが、この関数群では前半のKiloByte系を使い、後半のKibiByte系は別関数(HumanBytes2)で使われます。同じファイル内で両方の単位系を定数として用意し、用途によって使い分けているのがポイントです。

func HumanBytes(b int64) string {
	var value float64
	var unit string

関数シグネチャです。引数bはint64型(64ビットの符号付き整数)で受け取ったバイト数、戻り値はstring型の1つだけです。var value float64とvar unit stringは変数宣言で、値を代入していないので初期値(ゼロ値)が入ります。float64のゼロ値は0.0、stringのゼロ値は空文字列""です。この後のswitch文の中でこの2つの変数に値を詰めていきます。

switch {
	case b >= TeraByte:
		value = float64(b) / TeraByte
		unit = "TB"

switch の後に条件式を書かず { だけを置くと、「switch true」と同じ意味になり、各caseに書かれた真偽値を上から順に評価して、最初にtrueになったcaseの中身を実行します。if-elseif-elseの連続をswitchで書き直したものだと考えてください。case b >= TeraByte: は「bがTeraByte(1兆)以上なら」という条件で、一番大きい単位から先に判定しているのは、例えば5兆バイトのとき先にKiloByte以上に該当してしまわないようにするためです。

value = float64(b) / TeraByte

float64(b) はint64型の変数bをfloat64型に変換する型変換(キャスト)の書き方です。Goでは型が違う値同士の演算はできないため、bをint64のまま/TeraByte(型なし定数)で割ると整数除算になり小数点以下が切り捨てられてしまいます。それを避けるため先にfloat64に変換してから割り算し、value(float64型の変数)に代入しています。

default:
		return fmt.Sprintf("%d B", b)
	}

switch文の default節はどのcaseにも当てはまらなかったときに実行されます。ここではbがKiloByte未満、つまり1000バイト未満だったときの処理で、fmt.Sprintf("%d B", b)で「123 B」のような文字列を作ってすぐにreturnしています。ここでreturnすると関数はここで終了するので、この後の2つ目のswitchは実行されません。これが「早期return」というパターンで、単純なケースを先に片付けて、複雑な処理(後続コード)をネストさせずに済ませる書き方です。

switch {
	case value >= 10:
		return fmt.Sprintf("%d %s", int(value), unit)

1つ目のswitchでvalueとunitに値が入った後(例えばvalue=2.5, unit="GB")、2つ目のswitchで表示形式を決めています。value >= 10(値が2桁以上)のときは小数点を表示せず整数に丸めて表示します。int(value)はfloat64をint型に変換しており、Goのこの変換は小数点以下を切り捨てる(2.9でも2になる)ことを覚えておいてください。%dは整数、%sは文字列を埋め込むfmt.Sprintfの書式指定子です。

case value != math.Trunc(value):
		return fmt.Sprintf("%.1f %s", value, unit)

math.Trunc(value)はvalueの小数部分を切り捨てて整数部分だけを返す関数です(例: math.Trunc(2.5)は2.0)。value != math.Trunc(value)は「valueと、その整数部分が一致しない」つまり「小数部分がある」ことを判定するイディオムです。小数部分があれば%.1fという書式指定子で小数点以下1桁まで表示します(例: 2.5 GB)。

default:
		return fmt.Sprintf("%d %s", int(value), unit)
	}
}

10未満かつ小数部分もない(つまりvalueがちょうど3.0のようなきりのいい数)場合はここに落ちてきて、整数表示(3 GB)で返します。2つの条件どちらにも当てはまらなかった残りのケースをdefaultで拾うことで、switch全体が必ずどれか1つのcaseかdefaultを通り、関数が必ずstringを返すことが保証されています。

← 2026.09.09 の号を読む