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 * 1000const ( ... ) は複数の定数をまとめて宣言する構文です。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) / TeraBytefloat64(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を返すことが保証されています。