Day 1: Go言語の核心 - 解答例
チャレンジ1の解答: 型システムの理解
アプローチ1: 他言語からの移行者が最初に書くコード
package main
import "fmt"
// Python/JavaScriptなどの動的型付け言語からの移行者が
// 最初に試みるアプローチ - コンパイルエラーになる
/*
func add(a, b interface{}) interface{} {
// これはGoでは推奨されない
// interface{}は型安全性を失う
return a.(int) + b.(int) // ランタイムパニックのリスク
}
*/
// Java/C++のオーバーロードに慣れた人が期待するもの
// しかしGoには関数オーバーロードがない!
func addInt(a, b int) int {
return a + b
}
func addFloat64(a, b float64) float64 {
return a + b
}
func addInt64(a, b int64) int64 {
return a + b
}
func main() {
// 明示的な型宣言
var x int = 10
var y int = 20
fmt.Println("int:", addInt(x, y))
// 型推論(:=)を使用
a := 10.5
b := 20.3
fmt.Println("float64:", addFloat64(a, b))
// 明示的なキャスト(Goでは必須)
var i int = 10
var f float64 = 3.14
// result := i + f // コンパイルエラー!
result := float64(i) + f // 正しい方法
fmt.Println("mixed:", result)
// 型の異なる変数の演算には常にキャストが必要
var int32Num int32 = 100
var int64Num int64 = 200
// sum := int32Num + int64Num // コンパイルエラー!
sum := int64(int32Num) + int64Num // 明示的変換
fmt.Println("sum:", sum)
}
アプローチ2: Go 1.18以降のジェネリクス(推奨)
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// ジェネリクス関数 - 1つの実装で複数の型に対応
// constraints.Orderedは比較可能な型を表す
func Add[T constraints.Ordered](a, b T) T {
return a + b
}
// カスタム型制約の定義
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
// より厳密な数値型のみの制約
func AddNumbers[T Number](a, b T) T {
return a + b
}
// 複数の型パラメータを持つ関数
func Convert[T, U Number](value T) U {
return U(value)
}
func main() {
// 型推論が効く
fmt.Println("int:", Add(10, 20))
fmt.Println("float64:", Add(10.5, 20.3))
fmt.Println("string:", Add("hello", " world"))
// 明示的な型指定も可能
result := Add[int64](1000000000, 2000000000)
fmt.Println("int64:", result)
// 型変換を伴うジェネリクス
var x int32 = 100
var y float64 = Convert[int32, float64](x)
fmt.Println("converted:", y)
}
アプローチ3: インターフェースを使った多態性(Goらしい方法)
package main
import "fmt"
// 加算可能な型を表すインターフェース
type Addable interface {
Add(other Addable) Addable
}
// Int型(intのラッパー)
type Int int
func (i Int) Add(other Addable) Addable {
// 型アサーションで具体的な型を取得
if o, ok := other.(Int); ok {
return Int(int(i) + int(o))
}
panic("incompatible type")
}
// Float型(float64のラッパー)
type Float float64
func (f Float) Add(other Addable) Addable {
if o, ok := other.(Float); ok {
return Float(float64(f) + float64(o))
}
panic("incompatible type")
}
// ジェネリックな加算関数
func AddValues(a, b Addable) Addable {
return a.Add(b)
}
func main() {
x := Int(10)
y := Int(20)
fmt.Println("Int:", AddValues(x, y))
a := Float(10.5)
b := Float(20.3)
fmt.Println("Float:", AddValues(a, b))
}
最適化パス: どのアプローチを選ぶべきか
| アプローチ | 利点 | 欠点 | 使用場面 |
|---|---|---|---|
| 個別関数 | シンプル、読みやすい | コード重複 | 型が少ない場合 |
| ジェネリクス | コード再利用、型安全 | やや複雑 | Go 1.18以降で汎用的な処理 |
| インターフェース | 多態性、拡張性 | ボイラープレート | OOP的な設計が必要な場合 |
よくある間違い
package main
import "fmt"
func demonstrateCommonMistakes() {
// 間違い1: interface{}を乱用する(型安全性を失う)
var x interface{} = 10
var y interface{} = 20
// z := x + y // コンパイルエラー!interface{}は演算できない
// 間違い2: 型アサーションをチェックせずに使う
// result := x.(int) + y.(int) // パニックのリスク
// 正しい方法: カンマokイディオムで安全にチェック
if xi, ok := x.(int); ok {
if yi, ok := y.(int); ok {
fmt.Println("safe addition:", xi+yi)
}
}
// 間違い3: 異なる数値型を暗黙的に混ぜる(Python/JavaScriptの癖)
var a int32 = 10
var b int64 = 20
// c := a + b // コンパイルエラー!
c := int64(a) + b // 明示的変換が必要
fmt.Println("converted:", c)
// 間違い4: floatとintを混ぜる
var i int = 10
var f float64 = 3.14
// result := i + f // コンパイルエラー!
result := float64(i) + f
fmt.Println("result:", result)
}
ベンチマーク比較
package main
import (
"testing"
)
// 個別関数版
func addInt(a, b int) int {
return a + b
}
// ジェネリクス版
func Add[T int | int64 | float64](a, b T) T {
return a + b
}
// インターフェース版
type Addable interface {
Add(other Addable) Addable
}
type Int int
func (i Int) Add(other Addable) Addable {
return Int(int(i) + int(other.(Int)))
}
// ベンチマーク1: 個別関数
func BenchmarkIndividualFunc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = addInt(10, 20)
}
}
// ベンチマーク2: ジェネリクス
func BenchmarkGenerics(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(10, 20)
}
}
// ベンチマーク3: インターフェース
func BenchmarkInterface(b *testing.B) {
x := Int(10)
y := Int(20)
for i := 0; i < b.N; i++ {
_ = x.Add(y)
}
}
/*
ベンチマーク結果の例:
BenchmarkIndividualFunc-8 1000000000 0.25 ns/op
BenchmarkGenerics-8 1000000000 0.25 ns/op
BenchmarkInterface-8 500000000 3.50 ns/op
結論:
- 個別関数とジェネリクスはほぼ同じパフォーマンス(インライン化される)
- インターフェースは仮想関数呼び出しのオーバーヘッドがある
- パフォーマンスが重要な場合は個別関数かジェネリクスを選ぶ
*/
トレードオフ分析:
- 個別関数: 最も高速だが、型ごとにコードが増える。小規模なプロジェクトや特定の型のみを扱う場合に最適
- ジェネリクス: コンパイル時に型が解決されるため高速。コードの再利用性が高い。Go 1.18以降で推奨
- インターフェース: 実行時の柔軟性が高いが、若干のオーバーヘッドあり。プラグインシステムなど動的な型が必要な場合に使用
---
チャレンジ2の解答: ポインタとメモリ効率
他言語との比較
# Python - すべてが参照渡し(オブジェクトへの参照)
class LargeStruct:
def __init__(self):
self.data = [i for i in range(1000)]
def process(s):
return sum(s.data)
# 常に参照が渡される(コピーは発生しない)
large = LargeStruct()
result = process(large)
// JavaScript - オブジェクトは参照渡し
class LargeStruct {
constructor() {
this.data = Array.from({length: 1000}, (_, i) => i);
}
}
function process(s) {
return s.data.reduce((a, b) => a + b, 0);
}
// 常に参照が渡される
const large = new LargeStruct();
const result = process(large);
Go - 明示的な選択
package main
import (
"fmt"
"runtime"
"time"
"unsafe"
)
// 大きな構造体(8KB)
type LargeStruct struct {
Data [1000]int // 1000 * 8バイト = 8000バイト
}
// 中程度の構造体(40バイト)
type MediumStruct struct {
A, B, C, D, E int // 5 * 8バイト = 40バイト
}
// 小さな構造体(16バイト)
type SmallStruct struct {
X, Y int // 2 * 8バイト = 16バイト
}
// アプローチ1: 値渡し(コピーが発生)
func processByValue(s LargeStruct) int {
// s はコピー。元の構造体とは別のメモリ領域
// この関数内での変更は呼び出し元に影響しない
sum := 0
for _, v := range s.Data {
sum += v
}
return sum
}
// アプローチ2: ポインタ渡し(参照のみコピー)
func processByPointer(s *LargeStruct) int {
// s はポインタ(8バイト)。元の構造体を参照
// メモリコピーが発生しない
sum := 0
for _, v := range s.Data {
sum += v
}
return sum
}
// アプローチ3: 構造体の一部を変更する場合(値渡し)
func modifyByValue(s LargeStruct) LargeStruct {
// 変更したコピーを返す必要がある
s.Data[0] = 999
return s // さらにコピーが発生
}
// アプローチ4: 構造体の一部を変更する場合(ポインタ渡し)
func modifyByPointer(s *LargeStruct) {
// ポインタ経由で直接変更
// 返り値不要
s.Data[0] = 999
}
func main() {
// 構造体のサイズを確認
fmt.Printf("LargeStruct size: %d bytes\n", unsafe.Sizeof(LargeStruct{}))
fmt.Printf("Pointer size: %d bytes\n", unsafe.Sizeof(&LargeStruct{}))
large := LargeStruct{}
for i := range large.Data {
large.Data[i] = i
}
// ベンチマーク1: 値渡し
var m1 runtime.MemStats
runtime.ReadMemStats(&m1)
start := time.Now()
for i := 0; i < 100000; i++ {
processByValue(large)
}
elapsed1 := time.Since(start)
var m2 runtime.MemStats
runtime.ReadMemStats(&m2)
fmt.Printf("値渡し: %v, メモリ割り当て: %d MB\n",
elapsed1, (m2.TotalAlloc-m1.TotalAlloc)/1024/1024)
// ベンチマーク2: ポインタ渡し
var m3 runtime.MemStats
runtime.ReadMemStats(&m3)
start = time.Now()
for i := 0; i < 100000; i++ {
processByPointer(&large)
}
elapsed2 := time.Since(start)
var m4 runtime.MemStats
runtime.ReadMemStats(&m4)
fmt.Printf("ポインタ渡し: %v, メモリ割り当て: %d MB\n",
elapsed2, (m4.TotalAlloc-m3.TotalAlloc)/1024/1024)
// パフォーマンス比較
ratio := float64(elapsed1) / float64(elapsed2)
fmt.Printf("\nポインタ渡しは値渡しの %.2fx 高速\n", ratio)
// 変更のテスト
fmt.Println("\n--- 値渡しでの変更 ---")
before := large.Data[0]
modified := modifyByValue(large)
fmt.Printf("元の値: %d, 変更後の元の値: %d, 返された値: %d\n",
before, large.Data[0], modified.Data[0])
fmt.Println("\n--- ポインタ渡しでの変更 ---")
before = large.Data[0]
modifyByPointer(&large)
fmt.Printf("元の値: %d, 変更後の値: %d\n", before, large.Data[0])
}
小さな構造体の場合
package main
import (
"fmt"
"time"
)
type Point struct {
X, Y int
}
// 小さな構造体は値渡しの方が効率的な場合もある
func distanceByValue(p1, p2 Point) float64 {
dx := float64(p1.X - p2.X)
dy := float64(p1.Y - p2.Y)
return dx*dx + dy*dy
}
func distanceByPointer(p1, p2 *Point) float64 {
dx := float64(p1.X - p2.X)
dy := float64(p1.Y - p2.Y)
return dx*dx + dy*dy
}
func main() {
p1 := Point{X: 10, Y: 20}
p2 := Point{X: 30, Y: 40}
// 小さな構造体(16バイト)のベンチマーク
start := time.Now()
for i := 0; i < 10000000; i++ {
distanceByValue(p1, p2)
}
fmt.Println("値渡し:", time.Since(start))
start = time.Now()
for i := 0; i < 10000000; i++ {
distanceByPointer(&p1, &p2)
}
fmt.Println("ポインタ渡し:", time.Since(start))
}
/*
小さな構造体の場合:
値渡し: 8ms
ポインタ渡し: 12ms
理由:
- 小さな構造体(16バイト以下)はCPUレジスタに収まる
- ポインタの間接参照のコストの方が高い
- スタック割り当てはヒープより高速
*/
メモリレイアウトの可視化
package main
import (
"fmt"
"unsafe"
)
type Example struct {
A int32 // 4バイト
B int64 // 8バイト
C int32 // 4バイト
}
type OptimizedExample struct {
B int64 // 8バイト
A int32 // 4バイト
C int32 // 4バイト
}
func main() {
fmt.Println("=== メモリアライメント ===")
// パディングを含むサイズ
e := Example{}
fmt.Printf("Example size: %d bytes\n", unsafe.Sizeof(e))
fmt.Printf(" A offset: %d\n", unsafe.Offsetof(e.A))
fmt.Printf(" B offset: %d\n", unsafe.Offsetof(e.B))
fmt.Printf(" C offset: %d\n", unsafe.Offsetof(e.C))
// 最適化されたレイアウト
oe := OptimizedExample{}
fmt.Printf("\nOptimizedExample size: %d bytes\n", unsafe.Sizeof(oe))
fmt.Printf(" B offset: %d\n", unsafe.Offsetof(oe.B))
fmt.Printf(" A offset: %d\n", unsafe.Offsetof(oe.A))
fmt.Printf(" C offset: %d\n", unsafe.Offsetof(oe.C))
/*
出力例:
Example size: 24 bytes
A offset: 0
B offset: 8
C offset: 16
OptimizedExample size: 16 bytes
B offset: 0
A offset: 8
C offset: 12
フィールドの順序を変えるだけで24バイト→16バイトに削減!
*/
}
よくある間違い
package main
import "fmt"
type User struct {
Name string
Age int
}
// 間違い1: ポインタレシーバーを使うべき場所で値レシーバー
func (u User) IncrementAgeBad() {
u.Age++ // コピーに対する操作。元の値は変わらない
}
// 正しい方法: ポインタレシーバー
func (u *User) IncrementAge() {
u.Age++ // ポインタ経由で元の値を変更
}
// 間違い2: nilポインタのチェック忘れ
func printUserBad(u *User) {
fmt.Println(u.Name) // u が nil ならパニック!
}
// 正しい方法: nilチェック
func printUser(u *User) {
if u == nil {
fmt.Println("User is nil")
return
}
fmt.Println(u.Name)
}
// 間違い3: ループ内でのポインタの誤用
func collectUsersBad(names []string) []*User {
users := make([]*User, 0, len(names))
for _, name := range names {
u := User{Name: name}
users = append(users, &u) // 危険!同じアドレスを追加
}
return users
}
// 正しい方法
func collectUsers(names []string) []*User {
users := make([]*User, 0, len(names))
for _, name := range names {
u := User{Name: name}
users = append(users, &u) // 各イテレーションで新しいアドレス
}
return users
}
func main() {
// テスト1
user := User{Name: "Alice", Age: 25}
user.IncrementAgeBad()
fmt.Printf("Bad: %d\n", user.Age) // 25(変わらない)
user.IncrementAge()
fmt.Printf("Good: %d\n", user.Age) // 26
// テスト2
printUser(nil) // 安全
// printUserBad(nil) // パニック
// テスト3
names := []string{"Alice", "Bob", "Charlie"}
users := collectUsers(names)
for _, u := range users {
fmt.Println(u.Name)
}
}
ベンチマーク結果とパフォーマンス分析
package main
import (
"testing"
)
type Data struct {
Values [1000]int
}
// ベンチマーク: 大きな構造体の値渡し
func BenchmarkLargeStructByValue(b *testing.B) {
data := Data{}
for i := range data.Values {
data.Values[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
processLargeByValue(data)
}
}
// ベンチマーク: 大きな構造体のポインタ渡し
func BenchmarkLargeStructByPointer(b *testing.B) {
data := Data{}
for i := range data.Values {
data.Values[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
processLargeByPointer(&data)
}
}
func processLargeByValue(d Data) int {
sum := 0
for _, v := range d.Values {
sum += v
}
return sum
}
func processLargeByPointer(d *Data) int {
sum := 0
for _, v := range d.Values {
sum += v
}
return sum
}
/*
実行コマンド:
go test -bench=. -benchmem
結果の例:
BenchmarkLargeStructByValue-8 10000 150000 ns/op 8192 B/op 1 allocs/op
BenchmarkLargeStructByPointer-8 100000 15000 ns/op 0 B/op 0 allocs/op
分析:
- ポインタ渡しは10倍高速
- 値渡しは毎回8KBの割り当てが発生
- ポインタ渡しはメモリ割り当てゼロ
*/
ルール・オブ・サム:
- 構造体が小さい(< 数十バイト)→ 値渡し
- 構造体を変更する必要がある → ポインタレシーバー
- 構造体が大きい(> 数百バイト)→ ポインタ渡し
- 一貫性のため、メソッドはすべてポインタレシーバーかすべて値レシーバーに統一
- 疑問があれば、ポインタを使う(後で最適化可能)
---
チャレンジ3の解答: スライスの内部理解
他言語との比較
# Python - リストは動的配列
lst = []
for i in range(20):
lst.append(i)
# Pythonは内部で自動的にメモリを拡張(実装はC言語)
# ユーザーは容量を意識する必要がない
// JavaScript - 配列は動的
const arr = [];
for (let i = 0; i < 20; i++) {
arr.push(i);
}
// JavaScriptエンジンが自動で拡張
Goのスライス - 明示的なメモリ管理
package main
import (
"fmt"
"reflect"
"unsafe"
)
// スライスの内部構造を可視化
func printSliceHeader(name string, s []int) {
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("%s:\n", name)
fmt.Printf(" Data: %p (配列の先頭アドレス)\n", unsafe.Pointer(sh.Data))
fmt.Printf(" Len: %d (現在の要素数)\n", sh.Len)
fmt.Printf(" Cap: %d (容量)\n", sh.Cap)
fmt.Printf(" 実際のサイズ: %d bytes\n\n", sh.Cap*int(unsafe.Sizeof(int(0))))
}
func main() {
fmt.Println("=== スライスの成長パターン ===\n")
// 容量1から開始
s := make([]int, 0, 1)
printSliceHeader("初期状態", s)
// 要素を追加しながら容量の変化を観察
for i := 0; i < 20; i++ {
oldCap := cap(s)
oldData := (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data
s = append(s, i)
newCap := cap(s)
newData := (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data
if oldCap != newCap {
fmt.Printf("再割り当て発生!\n")
fmt.Printf(" 長さ: %d, 旧容量: %d → 新容量: %d\n",
len(s), oldCap, newCap)
fmt.Printf(" 成長率: %.2fx\n", float64(newCap)/float64(oldCap))
fmt.Printf(" 配列アドレス変更: %p → %p\n\n",
unsafe.Pointer(oldData), unsafe.Pointer(newData))
}
}
printSliceHeader("最終状態", s)
fmt.Println("=== Go 1.18以降の成長戦略 ===")
fmt.Println("- 容量 < 256: 2倍に成長")
fmt.Println("- 容量 >= 256: 約1.25倍に成長(より緩やか)")
fmt.Println("- 理由: 大きなスライスでのメモリ浪費を防ぐ")
}
スライスとメモリリーク
package main
import (
"fmt"
"runtime"
)
// 問題: 大きなスライスの一部だけを返す
func getFirstTenBad(data []byte) []byte {
// data全体の配列を参照し続ける
// 元の配列がGCされない
return data[:10]
}
// 解決策: 新しいスライスにコピー
func getFirstTenGood(data []byte) []byte {
// 新しい配列を作成し、必要な部分だけコピー
result := make([]byte, 10)
copy(result, data[:10])
// 元のdataは参照されず、GC可能
return result
}
func demonstrateMemoryLeak() {
// 100MBのデータ
largeData := make([]byte, 100*1024*1024)
for i := range largeData {
largeData[i] = byte(i % 256)
}
// 悪い例: 100MBのメモリを保持し続ける
_ = getFirstTenBad(largeData)
// 良い例: 10バイトだけ保持
_ = getFirstTenGood(largeData)
}
func main() {
var m runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m)
fmt.Printf("開始時: %d MB\n", m.Alloc/1024/1024)
demonstrateMemoryLeak()
runtime.GC()
runtime.ReadMemStats(&m)
fmt.Printf("処理後: %d MB\n", m.Alloc/1024/1024)
}
スライスの部分共有と意図しない変更
package main
import "fmt"
func demonstrateSharing() {
// 元のスライス
original := []int{1, 2, 3, 4, 5}
fmt.Println("元のスライス:", original)
// 部分スライス(同じ配列を共有)
sub := original[1:4]
fmt.Println("部分スライス:", sub)
// 部分スライスを変更
sub[0] = 999
fmt.Println("\n部分スライスを変更後:")
fmt.Println("元のスライス:", original) // [1 999 3 4 5] - 変わった!
fmt.Println("部分スライス:", sub) // [999 3 4]
// appendの動作
fmt.Println("\n=== appendの動作 ===")
a := make([]int, 3, 5)
a[0], a[1], a[2] = 1, 2, 3
fmt.Printf("a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))
b := a[:] // 同じ配列を共有
fmt.Printf("b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))
// 容量内でappend(配列を共有したまま)
b = append(b, 4)
fmt.Printf("\nappend後:")
fmt.Printf("a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))
fmt.Printf("b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))
// もう一度変更
b[0] = 999
fmt.Printf("\nb[0]を変更後:")
fmt.Printf("a: %v\n", a) // aも変わる(同じ配列)
fmt.Printf("b: %v\n", b)
// 容量を超えるappend(新しい配列が割り当てられる)
b = append(b, 5, 6)
fmt.Printf("\n容量超過のappend後:")
fmt.Printf("a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))
fmt.Printf("b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))
// もう一度変更
b[0] = 111
fmt.Printf("\nb[0]を変更後:")
fmt.Printf("a: %v\n", a) // aは変わらない(別の配列)
fmt.Printf("b: %v\n", b)
}
func main() {
demonstrateSharing()
}
事前割り当てによるパフォーマンス最適化
package main
import (
"fmt"
"testing"
)
// 悪い例: 容量を指定しない
func buildSliceBad() []int {
var s []int // nil slice, cap=0
for i := 0; i < 10000; i++ {
s = append(s, i) // 何度も再割り当て
}
return s
}
// 良い例: 事前に容量を確保
func buildSliceGood() []int {
s := make([]int, 0, 10000) // 事前に容量確保
for i := 0; i < 10000; i++ {
s = append(s, i) // 再割り当てなし
}
return s
}
// さらに良い例: 長さも指定してインデックスアクセス
func buildSliceBest() []int {
s := make([]int, 10000) // 長さと容量を確保
for i := 0; i < 10000; i++ {
s[i] = i // appendより高速
}
return s
}
func BenchmarkBuildSliceBad(b *testing.B) {
for i := 0; i < b.N; i++ {
buildSliceBad()
}
}
func BenchmarkBuildSliceGood(b *testing.B) {
for i := 0; i < b.N; i++ {
buildSliceGood()
}
}
func BenchmarkBuildSliceBest(b *testing.B) {
for i := 0; i < b.N; i++ {
buildSliceBest()
}
}
/*
ベンチマーク結果:
BenchmarkBuildSliceBad-8 5000 250000 ns/op 386000 B/op 18 allocs/op
BenchmarkBuildSliceGood-8 10000 100000 ns/op 81920 B/op 1 allocs/op
BenchmarkBuildSliceBest-8 15000 80000 ns/op 81920 B/op 1 allocs/op
分析:
- Bad: 18回も再割り当てが発生、メモリ使用量も多い
- Good: 1回の割り当てで済む、2.5倍高速
- Best: インデックスアクセスでappendのオーバーヘッドを回避、さらに高速
*/
よくある間違い
package main
import "fmt"
func main() {
fmt.Println("=== 間違い1: appendの結果を捨てる ===")
s1 := []int{1, 2, 3}
append(s1, 4) // 結果を代入していない!
fmt.Println(s1) // [1 2 3] - 変わっていない
s1 = append(s1, 4) // 正しい
fmt.Println(s1) // [1 2 3 4]
fmt.Println("\n=== 間違い2: スライスのコピーを忘れる ===")
s2 := []int{1, 2, 3}
s3 := s2 // 同じ配列を共有(コピーではない)
s3[0] = 999
fmt.Println("s2:", s2) // [999 2 3] - s2も変わる
fmt.Println("s3:", s3) // [999 2 3]
// 正しいコピー方法
s4 := make([]int, len(s2))
copy(s4, s2)
s4[0] = 111
fmt.Println("s2:", s2) // [999 2 3] - s2は変わらない
fmt.Println("s4:", s4) // [111 2 3]
fmt.Println("\n=== 間違い3: nilスライスとemptyスライスの混同 ===")
var nilSlice []int
emptySlice := []int{}
madeSlice := make([]int, 0)
fmt.Printf("nil slice: %v, len=%d, cap=%d, isNil=%v\n",
nilSlice, len(nilSlice), cap(nilSlice), nilSlice == nil)
fmt.Printf("empty slice: %v, len=%d, cap=%d, isNil=%v\n",
emptySlice, len(emptySlice), cap(emptySlice), emptySlice == nil)
fmt.Printf("made slice: %v, len=%d, cap=%d, isNil=%v\n",
madeSlice, len(madeSlice), cap(madeSlice), madeSlice == nil)
// すべて安全に使える
for _, s := range [][]int{nilSlice, emptySlice, madeSlice} {
s = append(s, 1)
fmt.Println(s)
}
}
スライスの内部構造の可視化
スライスの構造:
type slice struct {
ptr *[cap]T // 配列へのポインタ
len int // 現在の長さ
cap int // 容量
}
メモリレイアウト:
s := []int{1, 2, 3, 4, 5}
スライスヘッダ(24バイト on 64-bit):
┌─────────┬─────┬─────┐
│ ptr │ len │ cap │
│ 8 bytes│8bytes│8bytes│
└────┬────┴─────┴─────┘
│
└──> 実際の配列:
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴───┴───┴───┴───┘
部分スライス:
sub := s[1:4]
スライスヘッダ:
┌─────────┬─────┬─────┐
│ ptr │ len │ cap │
│ (s+8) │ 3 │ 4 │
└────┬────┴─────┴─────┘
│
└──> 同じ配列の別の位置:
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴─┬─┴───┴───┴───┘
└─ sub が指す位置
---
追加チャレンジ: ゼロ値を活用したコンフィグパターン
パターン1: デフォルト値の適用
package main
import "fmt"
type ServerConfig struct {
Host string
Port int
Timeout int
MaxConn int
Debug bool
}
// メソッドでデフォルト値を適用
func (c *ServerConfig) ApplyDefaults() {
if c.Host == "" {
c.Host = "localhost"
}
if c.Port == 0 {
c.Port = 8080
}
if c.Timeout == 0 {
c.Timeout = 30
}
if c.MaxConn == 0 {
c.MaxConn = 100
}
// Debug のデフォルトは false なので、そのまま
}
func main() {
// 一部だけ指定
config := ServerConfig{
Port: 3000,
Debug: true,
}
config.ApplyDefaults()
fmt.Printf("%+v\n", config)
// {Host:localhost Port:3000 Timeout:30 MaxConn:100 Debug:true}
}
パターン2: Functional Options Pattern(実践的)
package main
import (
"fmt"
"time"
)
type Server struct {
host string
port int
timeout time.Duration
maxConn int
debug bool
}
// Option関数型
type Option func(*Server)
// オプション設定関数
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConn(maxConn int) Option {
return func(s *Server) {
s.maxConn = maxConn
}
}
func WithDebug(debug bool) Option {
return func(s *Server) {
s.debug = debug
}
}
// コンストラクタ
func NewServer(opts ...Option) *Server {
// デフォルト値
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConn: 100,
debug: false,
}
// オプションを適用
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
// デフォルト値で作成
s1 := NewServer()
fmt.Printf("s1: %+v\n", s1)
// 一部だけカスタマイズ
s2 := NewServer(
WithPort(3000),
WithDebug(true),
)
fmt.Printf("s2: %+v\n", s2)
// 複数のオプション
s3 := NewServer(
WithHost("0.0.0.0"),
WithPort(9000),
WithTimeout(60*time.Second),
WithMaxConn(500),
)
fmt.Printf("s3: %+v\n", s3)
}
パターン3: ゼロ値で有効な型の設計
package main
import (
"fmt"
"sync"
)
// ゼロ値で使える型の良い例
type Counter struct {
mu sync.Mutex // ゼロ値で使える
count int // ゼロ値は0
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// ゼロ値で使える設計の別の例
type Buffer struct {
data []byte // nil でも append は動作する
}
func (b *Buffer) Write(p []byte) {
// b.data が nil でも動作する
b.data = append(b.data, p...)
}
func (b *Buffer) Bytes() []byte {
return b.data
}
func main() {
// 初期化不要で使える
var c Counter
c.Increment()
c.Increment()
fmt.Println("Counter:", c.Value())
var buf Buffer
buf.Write([]byte("Hello"))
buf.Write([]byte(" World"))
fmt.Println("Buffer:", string(buf.Bytes()))
}
このパターンは標準ライブラリでも多用されています(bytes.Buffer, strings.Builder, sync.Mutexなど)。