6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】非安全な unsafe パッケージを比較的安全に使うためのルール 3選

Last updated at Posted at 2025-12-15

これは、Go Advent Calendar 2025の16日目の記事です。

はじめに

こんにちは、こーが です。
先日の Go Conference 2025 では、defer について LT をさせていただきました。初めての LT で緊張しましたが、また機会を見つけて登壇したいなと思ってます。

さて本題ですが、Go 言語(以下、Go )は、「型安全性」が高い言語です。と同時に、ガベージコレクション(以下、GC )による自動メモリ管理や、ポインタ演算を許さない言語仕様により、「メモリ安全性」が高い言語でもあります。そのため、開発者が型システムを無視したり、メモリアドレスを直接操作したりすることは、Go の設計思想に反することと言えます。

それにもかかわらず、標準ライブラリには先述した操作を可能にする unsafeパッケージ (以下、unsafe ) という、その名の通り「安全ではない」パッケージが存在します。
本記事の前半では、この unsafe がなぜ存在するのか、また後半では、今後 unsafe を使わざるを得ない場面に備えるための 3 つのルールを紹介したいと思います。

対象読者

  • まだ unsafe を使ったことが無い方
  • 今後、C 言語との連携などで unsafe を使う必要がある方
  • unsafe を使った過酷なパフォーマンスチューニングをしている方

unsafe とは

まず、unsafe の概要として、unsafe を理解する上で重要な2つの型である unsafe.Pointeruintptr について紹介します。

unsafe.Pointer

unsafe.Pointer は、Go における 特別なポインタ型 です。主な特徴は以下の通りです。

  • 一言で言うと「どんな型のポインタにもなれる特別なポインタ」
  • 異なるポインタ型同士を変換するための「橋渡し役」として使われることが多い
  • unsafe.Pointer 自体を直接デリファレンスすることはできない
  • GC によって追跡される(後ほど説明します)

以下は unsafe.Pointer を使って stringint として読み取るサンプルです。
本来、p = q と書くとコンパイルエラーになりますが、unsafe.Pointer を経由することで、型チェックをすり抜けて変換できてしまうことが分かります。

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var p int
	var q string

	p = *(*int)(unsafe.Pointer(&q)) // string → unsafe.Pointer → int
	fmt.Println(p)                  // 0
}

uintptr

uintptr はポインタっぽいですが、単なる整数型です。 特徴は以下の通りです。

  • 一言で言うと「メモリアドレスを保持できるただの整数」
  • メモリアドレスの算術演算に使われることが多い
  • unsafe.Pointer と相互に変換できる
  • GC によって追跡されない(後ほど説明します)

以下は 「unsafe.Pointer から uintptr への変換」→「uintptr の演算」→「unsafe.Pointer への復帰」という一連の流れを示したサンプルです。
後述しますが、ここでは敢えて危険な書き方で記載しています。

var data uint64 = 42
ptr := unsafe.Pointer(&data)
fmt.Println(ptr, *(*uint64)(ptr)) // 0xc0000100a0 42

// ❌ 動くが、危険な書き方
u := uintptr(ptr)                   // unsafe.Pointer → uintptr
u += 8                              // 8 バイト進める。 uint64は8バイトのため、未定義領域を指す
ptr2 := unsafe.Pointer(u)           // uintptr → unsafe.Pointer
fmt.Println(ptr2, *(*uint64)(ptr2)) // 0xc0000100a8 0

unsafe に伴うリスク

先述の通り、unsafe は使い方次第で Go の「型安全性」や「メモリ安全性」を破壊できてしまうため、使用は最小限に留めることが推奨されています。特に、無邪気にメモリアドレスを操作すると GC の動作が不安定になったり、メモリ破壊によるクラッシュの原因となる可能性があります。

また、Go 1 の互換性保証ガイドライン (Go 1 and the Future of Go Programs)では、unsafe を含む一部のパッケージについて

Packages that import unsafe may depend on internal properties of the Go implementation. We reserve the right to make changes to the implementation that may break such programs.
unsafeをインポートするパッケージは、Goの実装の内部的な特性に依存する可能性があります。実装に対する変更によって、そのようなプログラムが動作しなくなる可能性がありますのでご注意ください。

と明記されています。これはつまり、将来の Go リリースで挙動が変わったり、突然動かなくなったり、意図しない副作用の可能性を示しており、これらを理解したうえで使う必要があるということです。

なぜ unsafe が存在するのか

では、なぜこのようにリスキーなパッケージを標準ライブラリとして提供しているのでしょうか?

2020 年に公開された研究論文 Breaking Type Safety in Go: An Empirical Study
on the Usage of the unsafe Package
によると、unsafe の利用動機は主に 2 つに分類されます。

パフォーマンスの最適化

1 つ目はパフォーマンスの最適化です。
unsafe を使うことで、Go の型システムや標準パッケージでは表現できない効率的なデータアクセスやメモリ操作を実現するために使われています。論文では、

Developers are willing to use unsafe to break language specifications (e.g., string immutability) for better performance
開発者は、より良いパフォーマンスを得るために、言語仕様(例えば、文字列の不変性)をunsafeを使って破壊することを厭わない。

と開発者たちが言語仕様を破ってでも性能を優先するケースがあることが示されていました。

OS や C 言語との連携などの低レベル操作

2 つ目は OS や C 言語との連携などの低レベル操作です。
Go は本来、型安全に設計された言語ですが、システムコールや既存の C ライブラリとの連携といった領域では、どうしても低レベルなメモリ操作が必要になります。論文では

However, to give developers the possibility of implementing low-level code, Go ships with a special package called unsafe that offers developers a way around the type safety of Go programs.
開発者が低レベルなコードを実装できるように、Go には "unsafe" と呼ばれる特別なパッケージが付属しており、これを利用することで Go プログラムの型安全性を回避できます。

とそのような「言語仕様だけでは表現しきれない要求」に応えるため、あえてその安全性を「外れる」ことを許可する仕組みとして unsafe を備えていることが示されていました。

unsafe を「怖がらず」に使うために

ここまでを読まれて「unsafe は危険だ!怖すぎる!!」と感じた方もいるかもしれません。
しかし、本記事を通して伝えたいのは、「unsafe 自体が危険というわけではない」ということです。問題は、unsafe を使うことで Go が本来保証してくれていた安全性を開発者が管理しなければならなくなるという点です。
つまり、危険なのは unsafe そのものではなく、unsafe を理解しないまま使ってしまうことにあります。

ここからは、そんな unsafe を「比較的安全に」扱うためのルールを紹介していければと思います。

ルール 1: uintptr の操作は単一の式で記述する

unsafe を使う上で基本的かつ、最もバグを生みやすいのがポインタのアドレス演算であると思います。

ここでのポイントは、unsafe.Pointer は GCによって追跡されるのに対し、uintptr は GCによって追跡されないという所です。

どういうことか?
例えば、GC は unsafe.Pointer のオブジェクトが移動したことを追跡することは可能ですが、uintptr はただの整数なため、オブジェクトが移動したことを検知できません(GC はその値がアドレスを指しているかどうかは認識できない)。つまりは、uintptr をポインタのように扱ってしまうと、メモリ安全性が失われてしまう可能性があるということです。

Gemini_Generated_Image_9l5yhg9l5yhg9l5y.png

この危険な状態をソースコードで示したのが先ほど紹介した「unsafe.Pointer から uintptr への変換」→「uintptr の演算」→「unsafe.Pointer への復帰」を行うサンプルです。

❌ 危険なコード例

var data uint64 = 42
ptr := unsafe.Pointer(&data)
fmt.Println(ptr, *(*uint64)(ptr)) // 0xc0000100a0 42

u := uintptr(ptr)                   // unsafe.Pointer → uintptr
u += 8                              // 8バイト進める。 uint64は8バイトのため、未定義領域を指す
// --- ❌ もしここでGCが走り、data が移動したら u は無効なアドレスとなってしまう可能性がある ---
ptr2 := unsafe.Pointer(u)           // uintptr → unsafe.Pointer
fmt.Println(ptr2, *(*uint64)(ptr2)) // ???

そのため、uintptr の演算処理はコンパイラが分割できない「単一の式」で行う必要があります。

✅ 比較的安全なコード例(単一の式)

var data uint64 = 42
ptr := unsafe.Pointer(&data)
fmt.Println(ptr, *(*uint64)(ptr)) // 0xc0000100a0 42

ptr2 := unsafe.Pointer(uintptr(ptr) + 8) // ✅
fmt.Println(ptr2, *(*uint64)(ptr2)) // 0xc0000100a8 0

また、Go 1.17 では関数 unsafe.Add が導入されました。これを使うことで uintptr を記載する余地をなくすことができます。特別な理由がない限りはこちらがオススメです。

✅ 比較的安全なコード例(unsafe.Add)(Go 1.17+)

var data uint64 = 42
ptr := unsafe.Pointer(&data)
fmt.Println(ptr, *(*uint64)(ptr)) // 0xc0000100a0 42

ptr2 := unsafe.Add(ptr, 8) // ✅
fmt.Println(ptr2, *(*uint64)(ptr2)) // 0xc0000100a8 0

現在の Go の GC は「non-moving GC」であるため、画像のようなアドレス移動は発生しません。(参考:A Guide to the Go Garbage Collector)

ここでの問題点である「uintptr は GC によって追跡されない」ということのリスクを伝える例として書いてますことをご了承ください。
(実際、GC の将来的な実装変更や最適化によって non-moving GC でなくなる可能性もあることは「unsafe に伴うリスク」で説明した通りです)

ルール 2: reflect.Header 系は使わない

かつて、Go の内部データ構造(スライスや文字列の中身)を操作するために、reflect.SliceHeaderreflect.StringHeader という構造体が使われていました。しかし、これらは現在 deprecated (非推奨)であり、使うべきではありません。

なぜ使ってはいけないのか?
これらの構造体は、内部のポインタを uintptr として保持しています。

// reflect.SliceHeader
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

// reflect.StringHeader
type StringHeader struct {
	Data uintptr
	Len  int
}

ルール 1で説明した通り、uintptr はGCの追跡対象外です。これらの構造体を使っている間に GC が走ると、予期しない動作をしてしまうリスクが常にありました。

Go 1.17 では 新しいスライスを生成する関数 unsafe.Slice が、そして、Go 1.20 では新しい文字列を生成する関数 unsafe.Stringの他に、先頭要素や先頭バイトのポインタを返す関数 unsafe.SliceDataunsafe.StringData が導入されました。これらを使うことで、reflect パッケージに依存せず、より安全にスライスや文字列の内部にアクセスできます。

以下は StringHeader を使い、スライス内部の Cap を操作するサンプルと、これを unsafe.Sliceunsafe.SliceData で書き直したサンプルです。
❌ 危険なコード例

src := []int{1, 2, 3}

hdr := (*reflect.SliceHeader)(unsafe.Pointer(&src)) // スライスの内部構造である reflect.SliceHeader を直接取得
hdr.Cap = 100                                       // 強引に cap を拡張する
// --- ❌ もしここでGCが走り、src が移動したら hdr は無効なアドレスとなってしまう ---
fmt.Println(len(src), cap(src), src)                // 3 100 [1 2 3]

✅ 比較的安全なコード例(unsafe.Slice 、unsafe.SliceData)(Go 1.20+)

src := []int{1, 2, 3}

p := unsafe.SliceData(src)           // []int → スライスの先頭要素へのポインタ (ここでは、*int)
dst := unsafe.Slice(p, 100)[:3]      // スライスの先頭要素へのポインタ  → 新しいスライス(len=3, cap=100)を生成
fmt.Println(len(dst), cap(dst), dst) // 3 100 [1 2 3]

ルール 3: ゼロコピー変換は読み取り専用で使う

Webサーバーなどで巨大なリクエストボディ(byte)を文字列(string)として扱いたい場合、通常のキャスト string(b) を行うと、Go は安全のためにメモリをコピーします。このコピーコストを削減するために使われるのが「ゼロコピー変換」と呼ばれる必殺技です。

ゼロコピー変換の書き方は、ルール 2 で紹介した unsafe の関数を使うと、以下のように書けます。

var b []byte = []byte{'G', 'o', '!'}

p := unsafe.SliceData(b)
s := unsafe.String(p, len(b))
fmt.Println(s) // Go!

Goの string 型は「不変(Immutable)」です。しかし、上記のコードで作成した文字列 sは、元のバイトスライス b とメモリを共有しています。

もし、変換後に b の内容を書き換えるとどうなるでしょうか?

var b []byte = []byte{'G', 'o', '!'}

p := unsafe.SliceData(b)
s := unsafe.String(p, len(b))
fmt.Println(s) // Go!

b[0] = 'N'
fmt.Println(s) // No! 

s も「No!」に変わってしまいました。

これは結果的に「文字列は不変である」という Go の前提を破壊する行為であるため、unsafe.String によって string として解釈されている元のメモリや、unsafe.StringData によって返されるバイト列は、絶対に変更してはいけません。

例えば、map のキーとして使っている文字列の中身が変わってしまった場合に、ハッシュ値が狂い、データを取り出せなくなるだけでなく、プログラム全体が予期せぬ挙動を起こしてしまう可能性があります。

ゼロコピー変換を使う場合は、内部のバイト列を書き換えられないことが保証されている場合にのみ使用しましょう。

まとめ

本記事では、unsafe を比較的安全に使うための 3 つのルールを紹介しました。

  • ポインタ演算は unsafe.Add を使うか、uintptr の変換を単一の式内で記述する

  • reflect.SliceHeader / StringHeader ではなく、unsafe.Slice / SliceData / String / StringData を使用する

  • ゼロコピー変換は「読み取り専用」でのみ使用する

ちゃぶ台をひっくり返すようですが、unsafe を使わないことが最も安全です。本記事のサンプルコードも、多くは unsafe を使わずに書けるものを敢えて unsafe を用いて紹介しています。
それでもなお、極限のパフォーマンスが必要な場合(?)や、Go の深淵に触れる(?)際には、本記事で紹介した 3 つのルールを思い出してもらえたらと思います!

参考

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?