まえがき
C言語でいう所のsizeofはGo言語にあるんだろうか?🤔という所が気になったので調べてみました。
早速結論ですが、Go言語で型のサイズを取得する場合はunsafe.Sizeof関数を使用します。
では使い方を見ていきましょう。
unsafe.Sizeof関数を使って型のサイズを取得する
使い方はとても簡単で、Sizeof関数の引数にサイズを取得したい変数を指定してあげるだけです。
var a int
typeSize := unsafe.Sizeof(a)
fmt.Printf("unsafe.Sizeof関数で取得したint型のサイズ:%dバイト\n", typeSize)
unsafe.Sizeof関数で取得したint型のサイズ:8バイト
実行するとint型は8バイトという結果になりました。
ですがint型のサイズは環境に依存するため、4バイトや8バイトなどに変化することに注意してください。
ちなみにSizeof関数で取得するとuintptr型の値となっているため、演算で使用する時に不便です。
演算で使用したい場合はint型にキャストしてあげましょう。
arraySize := 8
totalSize := int(typeSize) * arraySize
fmt.Printf("要素数%d個のint配列のサイズ:%dバイト\n", arraySize, totalSize)
要素数8個のint配列のサイズ:64バイト
では次に、reflectを使用した取得方法もありますのでそちらも見ていきましょう。
reflect.TypeOf関数を使って型のサイズを取得する
こちらもunsafeのSizeof関数と同様に、Typeof関数の引数にサイズを取得したい変数を指定してSize関数を呼ぶだけです。
var b int
typeSize = reflect.TypeOf(b).Size()
fmt.Printf("reflect.TypeOf関数で取得したint型のサイズ:%dバイト\n", typeSize)
reflect.TypeOf関数で取得したint型のサイズ:8バイト
当然ながらどちらの関数を使用しても結果は一緒になります。
unsafeとreflectのメリット・デメリット
どっちがいいの?って感じですがメリットとデメリットは下記になります。
unsafeパッケージ | reflectパッケージ | |
|---|---|---|
| メリット | - パフォーマンスの最適化 - 直接的なメモリアクセス - 外部システムとのインターフェース | - 型安全性の確保 - 動的なプログラミングのサポート |
| デメリット | - 型安全性の喪失 - メンテナンスの難しさ - ポータビリティの問題 | - パフォーマンスへの影響 - コードの複雑性の増加 |
それぞれ詳細を見ていきましょう。
unsafeのメリット
パフォーマンスの最適化
unsafeパッケージを使用すると、Goのランタイムオーバーヘッドを回避し、メモリアクセスやデータ構造の操作をより効率的に行うことができます。
これは、特にパフォーマンスが重要な低レベルのシステムプログラミングや、高速な実行が必要なアプリケーションで有利です。
直接的なメモリアクセス
unsafeパッケージを使用することで、プログラマーはメモリの特定のアドレスに直接アクセスすることができます。
これにより、C言語のような低レベルプログラミングの柔軟性とパワーをGoで実現することが可能になります。
外部システムとのインターフェース
外部ライブラリやシステムとのインターフェースを作成する際、unsafeパッケージはC言語の構造体や関数にGoから直接アクセスするために必要な場合があります。
これは、C言語で書かれたライブラリをGoから利用する際に特に有用です。
unsafeのデメリット
型安全性の喪失
unsafeパッケージの使用は、Goの強力な型安全性を犠牲にします。
不正確なメモリアクセスやポインタの操作は、予期しないバグやセグメンテーションフォールトを引き起こす可能性があります。
メンテナンスの難しさ
unsafeコードは理解しにくく、メンテナンスが難しくなることがあります。
特に、メモリレイアウトに依存するコードは、Goのランタイムやコンパイラのアップデートによって影響を受けやすいです。
ポータビリティの問題
unsafeを使用したコードは、異なるプラットフォームやアーキテクチャ間での移植性が低下する可能性があります。
メモリレイアウトやポインタのサイズはプラットフォームによって異なるため、プラットフォーム間での互換性を保証することが難しくなります。
特にデメリットはGoのいい部分を壊しかねないのでよく理解しておくべきです。
Reflectのメリット
型安全性
reflectパッケージを使用することで、unsafeパッケージを避けることができます。
なぜ避けるという表現を使うのかというと、unsafeパッケージはポインタ演算や任意の型へのキャストなど、型安全性を回避する操作を可能にしますが、これらの操作はバグやメモリ安全性の問題を引き起こすリスクがあります。
reflectを使用することで、これらのリスクを避けつつ、実行時に型情報を取得することができます。
動的なプログラミング
reflectを使用すると、プログラムが実行時に型を検査し、条件に応じて異なるアクションを取ることが可能になります。
これにより、汎用的な関数やライブラリを作成することができ、コードの再利用性が高まります。
Reflectのデメリット
パフォーマンス
reflectの使用はパフォーマンスに影響を与える可能性があります。
実行時の型検査や値の操作は、コンパイル時に型が既知の操作よりも時間がかかることが多いです。
特に、ループ内やパフォーマンスが重要なコードパスでのreflectの使用は、アプリケーションの全体的なパフォーマンスに影響を与える可能性があります。
複雑性の増加
reflectを使用するとコードの理解が難しくなる場合があります。
実行時にのみ明らかになる型の情報を扱うため、コードの読み手がその振る舞いを正確に把握するのが難しくなることがあります。
上記のようにパフォーマンスや可読性とのバランスを考えて使用してくことが大切という事ですね。
次は色々な型のサイズを調べて、想像と違ったものをピックアップしてみましたので見ていきましょう。
string型のサイズを調べてみる
string型のサイズがどういう結果になるのか見ていきましょう。
var c string
typeSize = unsafe.Sizeof(c)
fmt.Printf("string型のサイズ:%dバイト\n", typeSize)
string型のサイズ:16バイト
16バイト???🙃
何やら斜め上な結果が返ってきましたが、stringの実行時の実体は下記の構造体になっているようです。
type StringHeader struct {
Data uintptr
Len int
}
構造体を指定すると各メンバのサイズの合計が返ってくるため、16バイトだったというオチですね。
では文字数を取得したい場合はどうするかというと、
バイト数を取得したいと文字数を取得したい場合とで、下記のように使用する関数が違ってきます。
バイト単位の長さの取得
len関数を使って、string型のバイト単位の長さを取得する例です。
s := "Hello, 世界"
fmt.Println("バイト単位の長さ:", len(s)) // ASCII文字とマルチバイト文字の合計バイト数
この例では、“Hello, 世界"という文字列にはASCII文字とマルチバイト文字が含まれています。
“世界"はUTF-8でエンコードされており、各文字は3バイトを占めるため、バイト単位の長さはより大きな値になります。
実際の文字数の取得
UTF-8エンコードされた文字列の実際の文字数を取得するには、以下のようにutf8.RuneCountInString関数を使用します。
import "unicode/utf8"
s := "Hello, 世界"
fmt.Println("実際の文字数:", utf8.RuneCountInString(s)) // 実際の文字数
この関数は、マルチバイト文字を正しくカウントし、ユーザーが期待する「文字数」を返します。
この例では、“Hello, 世界"の実際の文字数は9文字となります。
このように、Go言語において、string型の文字数を取得する際は、単純にlen関数を使用するだけでなく、文字列がUTF-8エンコードされている場合はutf8.RuneCountInString関数を使用することで、正確な文字数を得ることができます。
これにより、国際化されたアプリケーションの開発時に文字列操作を正確に行うことが可能になります。
次はスライスのサイズを見ていきましょう。
スライスのサイズを調べてみる
string型が構造体のサイズだったので、こちらも同じでは?という推測がたちますがどうでしょうか。
var d []int
typeSize = unsafe.Sizeof(d)
fmt.Printf("スライスのサイズ:%dバイト\n", typeSize)
スライスのサイズ:24バイト
推測した通り構造体のサイズになっているようです。
スライスの構造体は下記のとおりです。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
スライスに対してsizeof関数を使うとスライスの要素数に応じたサイズが返ってくると思いがちですが、残念ながら動的なサイズではなく静的なサイズが返されます。
ここらへんを勘違いしてしまう方は結構いそうです🙄
スライスのサイズはlenとcapで取得しましょうということですね。
lentとcapを使用したスライスのサイズを取得する簡単なコード例は下記になります。
s := []int{1, 2, 3, 4, 5}
// スライスの長さ(動的なサイズ)を取得
length := len(s)
// スライスの容量(動的な容量)を取得
capacity := cap(s)
fmt.Printf("スライスの長さ: %d\n", length)
fmt.Printf("スライスの容量: %d\n", capacity)
この例では、スライスsに5つの要素が含まれているため、len(s)は5を返し、スライスの容量も(この場合は同じく)5を返します。スライスの容量は、スライスに追加できる要素の最大数を示します。
では次に、もう想像がついてますが一応マップのサイズも見ていきましょう。
マップのサイズを調べてみる
var e map[string]int
typeSize = unsafe.Sizeof(e)
fmt.Printf("マップのサイズ:%dバイト\n", typeSize)
マップのサイズ:8バイト
想像どおりでした🙂
恐らくマップの構造体のポインタのサイズになっていますね。
マップの実装は複雑なので、興味のある方はソースコード を見て頂ければと思います。
マップについてもlenでサイズを取得することができます。
マップの場合、len関数はマップに含まれるキー/値ペアの数(動的なサイズ)を返します。
m := map[string]int{"apple": 5, "banana": 2, "orange": 8}
// マップのサイズ(動的なサイズ)を取得
size := len(m)
fmt.Printf("マップのサイズ: %d\n", size)
この例では、マップmに3つのキー/値ペアが含まれているため、len(m)は3を返します。
まとめ
いかがでしたか。
私はsizeof関数を通して、内部の実装についてより理解が深まりました。
Go言語ではあまり使う機会がないと思いますが、どういうものか理解しておくと後々役に立つこともあると思うのでぜひ色々調べてみて下さい。
プログラム全体
コピーしてGo Playground に貼り付ければ実行できますので、実際の動作を確認してみてください。
package main
import (
"fmt"
"reflect"
"unicode/utf8"
"unsafe"
)
func main() {
//---------------------------------------------------------
// unsafe.Sizeof関数を使って型のサイズを取得する
//---------------------------------------------------------
var a int
typeSize := unsafe.Sizeof(a)
fmt.Printf("unsafe.Sizeof関数で取得したint型のサイズ:%dバイト\n", typeSize)
// uintptr型をint型にキャスト
arraySize := 8
totalSize := int(typeSize) * arraySize
fmt.Printf("要素数%d個のint配列のサイズ:%dバイト\n", arraySize, totalSize)
//---------------------------------------------------------
// reflect.TypeOf関数を使って型のサイズを取得する
//---------------------------------------------------------
var b int
typeSize = reflect.TypeOf(b).Size()
fmt.Printf("reflect.TypeOf関数で取得したint型のサイズ:%dバイト\n", typeSize)
//---------------------------------------------------------
// string型のサイズを調べてみる
//---------------------------------------------------------
var c string
typeSize = unsafe.Sizeof(c)
fmt.Printf("string型のサイズ:%dバイト\n", typeSize)
s := "Hello, 世界"
fmt.Println("バイト単位の長さ:", len(s)) // ASCII文字とマルチバイト文字の合計バイト数
s = "Hello, 世界"
fmt.Println("実際の文字数:", utf8.RuneCountInString(s)) // 実際の文字数
//---------------------------------------------------------
// スライスのサイズを調べてみる
//---------------------------------------------------------
var d []int
typeSize = unsafe.Sizeof(d)
fmt.Printf("スライスのサイズ:%dバイト\n", typeSize)
slice := []int{1, 2, 3, 4, 5}
// スライスの長さ(動的なサイズ)を取得
length := len(slice)
// スライスの容量(動的な容量)を取得
capacity := cap(slice)
fmt.Printf("スライスの長さ: %d\n", length)
fmt.Printf("スライスの容量: %d\n", capacity)
//---------------------------------------------------------
// マップのサイズを調べてみる
//---------------------------------------------------------
var e map[string]int
typeSize = unsafe.Sizeof(e)
fmt.Printf("マップのサイズ:%dバイト\n", typeSize)
m := map[string]int{"apple": 5, "banana": 2, "orange": 8}
// マップのサイズ(動的なサイズ)を取得
size := len(m)
fmt.Printf("マップのサイズ: %d\n", size)
}
unsafe.Sizeof関数で取得したint型のサイズ:8バイト
要素数8個のint配列のサイズ:64バイト
reflect.TypeOf関数で取得したint型のサイズ:8バイト
string型のサイズ:16バイト
バイト単位の長さ: 13
実際の文字数: 9
スライスのサイズ:24バイト
スライスの長さ: 5
スライスの容量: 5
マップのサイズ:8バイト
マップのサイズ: 3
実行環境
$ go version
go version go1.18 linux/amd64