構造体

Go言語の構造体コピーの基本と注意点について解説

Go言語で構造体をコピーする際の基本的な方法や注意点について解説します。

struct変数が値渡しされるポイントや、参照を利用する場合の違い、正しくコピーするためのコツをシンプルな例を交えながらご紹介します。

基本的な構造体コピーの方法

Go言語では、構造体は基本的に値コピーが行われます。

各フィールドが個別にコピーされるため、元の構造体とコピー先は独立した値として扱われます。

一方、ポインタ型のフィールドが存在する場合は、参照としてコピーされるため、同じメモリアドレスを指すことになります。

以下では、値コピーの動作や参照コピーの特徴について詳しく説明します。

値コピーの動作と特徴

構造体同士の代入を行うと、各フィールドの値がコピーされます。

たとえば、単純な構造体の場合、次のようにコピーを実施すると元の構造体を変更してもコピー先には影響がありません。

  • 値コピーの特徴

・構造体全体が一度にコピーされる

・基本型や値型フィールドは独立して扱われる

・メモリ上に別の領域が確保される

値コピーによる動作は、予期した挙動を示すため、コピーメカニズムを理解している場合は安心して利用できます。

参照コピーとポインタの扱い

構造体にポインタ型のフィールドが含まれている場合、代入によってポインタがコピーされるだけです。

そのため、コピー先と元の両方が同じアドレスのデータを参照することになります。

ポインタフィールドの挙動

ポインタのフィールドは、構造体のコピー時に値そのものがコピーされず、ポインタが指すデータの参照がコピーされます。

たとえば、下記のコードでは、構造体 Data がポインタフィールド Value を持ち、コピー後にどちらも同じ値を参照します。

package main
import "fmt"
// Data構造体はポインタフィールドを持つ
type Data struct {
	Value *int // ポインタフィールド
}
func main() {
	originalValue := 100
	original := Data{Value: &originalValue} // オリジナルの構造体
	// 値コピーによってポインタ自体がコピーされる
	copyData := original
	// コピー元とコピー先のアドレスを表示
	fmt.Printf("original.Value address: %p\n", original.Value)
	fmt.Printf("copyData.Value address: %p\n", copyData.Value)
	// コピー先の値を変更すると、オリジナルにも影響する
	*copyData.Value = 200
	fmt.Printf("変更後: original.Value = %d, copyData.Value = %d\n", *original.Value, *copyData.Value)
}
original.Value address: 0xxxxxxx
copyData.Value address: 0xxxxxxx
変更後: original.Value = 200, copyData.Value = 200

ネストした構造体のケース

構造体が他の構造体をフィールドとして持つ場合も、基本的には値コピーが行われます。

しかし、ネストした構造体内にポインタ型のフィールドがあれば、上記のポインタ挙動が適用されます。

たとえば、次の例では、Address 構造体が Person 構造体のフィールドとして定義されています。

Address 内のポインタ型フィールドが存在すると、コピー時に参照共有される点に注意が必要です。

package main
import "fmt"
// Addressは住所情報を表す構造体
type Address struct {
	Street *string // ポインタフィールド
	City   string
}
// Personは人物情報を表す構造体
type Person struct {
	Name    string
	Age     int
	Address Address // ネストした構造体
}
func main() {
	streetName := "123 Go Road"
	address := Address{
		Street: &streetName,
		City:   "Go言語 City",
	}
	original := Person{
		Name:    "Alice",
		Age:     30,
		Address: address,
	}
	// 値コピー
	copyPerson := original
	// アドレスのポインタ部分のアドレスを表示
	fmt.Printf("original.Address.Street address: %p\n", original.Address.Street)
	fmt.Printf("copyPerson.Address.Street address: %p\n", copyPerson.Address.Street)
	// コピー先の値変更
	*copyPerson.Address.Street = "456 New Road"
	fmt.Printf("変更後: original.Address.Street = %s, copyPerson.Address.Street = %s\n", *original.Address.Street, *copyPerson.Address.Street)
}
original.Address.Street address: 0xyyyyyy
copyPerson.Address.Street address: 0xyyyyyy
変更後: original.Address.Street = 456 New Road, copyPerson.Address.Street = 456 New Road

実装例で確認する構造体コピー

ここでは、実際のコードサンプルを用いて、構造体コピーの動作を確認します。

シンプルなコード例と、ネストした構造体を含む例の両方を解説します。

シンプルなコピーコード解説

以下の例は、基本的な構造体コピーの方法を示しています。

構造体 Person には NameAge のフィールドがあり、値コピーを行ったときの動作を確認します。

package main
import "fmt"
// Personは人物情報を表すシンプルな構造体
type Person struct {
	Name string
	Age  int
}
func main() {
	// originalPersonの初期化
	originalPerson := Person{
		Name: "Bob",
		Age:  25,
	}
	// 値コピーによって個別のインスタンスが作成される
	copiedPerson := originalPerson
	// コピー前の内容確認
	fmt.Printf("コピー前: originalPerson = %+v, copiedPerson = %+v\n", originalPerson, copiedPerson)
	// copiedPersonのAgeを変更
	copiedPerson.Age = 30
	// コピー後の内容確認
	fmt.Printf("コピー後: originalPerson = %+v, copiedPerson = %+v\n", originalPerson, copiedPerson)
}
コピー前: originalPerson = {Name:Bob Age:25}, copiedPerson = {Name:Bob Age:25}
コピー後: originalPerson = {Name:Bob Age:25}, copiedPerson = {Name:Bob Age:30}

ネスト構造体のコピー実例

次の例では、ネストした構造体とポインタ型フィールドを含む場合のコピー挙動を示します。

Person 構造体は Address 構造体をフィールドとして持っており、ポインタ型の Street フィールドが参照共有されることを確認できます。

package main
import "fmt"
// Addressは住所情報を表す構造体
type Address struct {
	Street *string // ポインタフィールド
	City   string
}
// Personは人物情報を表す構造体
type Person struct {
	Name    string
	Age     int
	Address Address // ネストした構造体
}
func main() {
	// 初期の住所情報
	street := "789 Old St"
	address := Address{
		Street: &street,
		City:   "Go Town",
	}
	// originalPersonの初期化
	originalPerson := Person{
		Name:    "Charlie",
		Age:     40,
		Address: address,
	}
	// 値コピー
	copiedPerson := originalPerson
	// コピー前の状態確認
	fmt.Printf("コピー前: originalPerson = %+v, copiedPerson = %+v\n", originalPerson, copiedPerson)
	// copiedPersonの住所のStreetを変更
	*copiedPerson.Address.Street = "1010 New Blvd"
	// コピー後の内容確認(Streetは参照共有のため両方に反映)
	fmt.Printf("コピー後: originalPerson.Address.Street = %s, copiedPerson.Address.Street = %s\n", *originalPerson.Address.Street, *copiedPerson.Address.Street)
}
コピー前: originalPerson = {Name:Charlie Age:40 Address:{Street:0xzzzzzz City:Go Town}}, copiedPerson = {Name:Charlie Age:40 Address:{Street:0xzzzzzz City:Go Town}}
コピー後: originalPerson.Address.Street = 1010 New Blvd, copiedPerson.Address.Street = 1010 New Blvd

構造体コピー時の注意点

構造体コピーを行う際に注意が必要な点について、以下に詳しく説明します。

意図しない参照共有が起こる可能性があるため、コピーする内容によっては深い検証が求められます。

意図しない参照共有の問題

値コピーを行った場合でも、ポインタ型のフィールドが存在すると、コピー元とコピー先で同じメモリアドレスを共有することがあります。

これが原因で、どちらか一方の変更がもう一方に反映され、思わぬバグを引き起こす可能性があります。

そのため、構造体の設計時にコピーの方法を十分に検討する必要があります。

コピー前の検証ポイント

構造体のコピーを行う前に、以下のポイントを確認することが推奨されます。

各フィールドの確認方法

・各フィールドが値型なのかポインタ型なのかを確認する

・ポインタ型の場合、意図的に参照共有を行うのか、完全な独立性を求めるのか判断する

・必要に応じて、コピー時に個別の初期化を実施する

メモリアドレスの検証

コピー前とコピー後で、ポインタ型フィールドが同一のメモリアドレスを持っているかを検証することで、意図しない参照共有を防止できます。

具体的には、fmt.Printf を使ってアドレスを出力し、異なるアドレスが確保されるように工夫する方法があります。

また、必要であればディープコピーの関数を実装して使用することも検討してください。

まとめ

この記事では、Go言語の構造体コピーの基本的な方法と注意点について、値コピーと参照コピーの違いやネスト構造体での挙動を具体的な実装例を通じて解説しましたでした。

全般として、構造体の値コピーは単純で扱いやすい一方、ポインタ型フィールドがある場合は参照共有に留意すべき点が明確になりました。

ぜひ実際にコードを書いて試し、適切なコピー方法を活用してみてください。

関連記事

Back to top button
目次へ