カテゴリ: Go言語 更新日: 2026/03/18

Go言語のスライスを引数に渡す際の注意点!値渡しと参照渡しをわかりやすく解説

Go言語のスライスを引数に渡す際の注意点(値渡しと参照渡し)
Go言語のスライスを引数に渡す際の注意点(値渡しと参照渡し)

先生と生徒の会話形式で理解しよう

生徒

「先生、Go言語でスライスを関数に渡すとき、値渡しとか参照渡しとか聞くんですが、どういう意味ですか?」

先生

「いい質問です。プログラミングでの値渡しと参照渡しは少し難しい言葉ですが、簡単に言うと『関数にデータを渡すとき、コピーして渡すのか、それとも元のデータを直接渡すのか』という違いです。」

生徒

「なるほど。でもスライスはどうなるんですか?」

先生

「それが少し特殊で、スライスは『見た目は値渡しだけど、中身は参照渡しに近い』という特徴があります。これから詳しく説明しますね。」

1. 値渡しと参照渡しって何?

1. 値渡しと参照渡しって何?
1. 値渡しと参照渡しって何?

まず、「関数に渡したあと、元の変数が変わるのか?」という視点で考えると分かりやすいです。Go言語では基本は値渡しですが、ポインタ(住所のメモ)を使えば参照渡し風の振る舞いを実現できます。

  • 値渡し(Call by Value):データのコピーを渡す方法。関数内で変更しても元の変数は変わりません。
  • 参照渡し(Call by Reference):データが置いてある場所(アドレス)を渡す方法。関数内の変更が元の変数に反映されます。

イメージで言うと、値渡しは「書類のコピーを手渡す」、参照渡しは「本棚の場所メモを渡す」感じです。コピーを書き換えても原本は無傷、場所メモなら原本そのものを書き換えられます。

超シンプルな例(整数で体験)


package main

import "fmt"

// 値渡し:nのコピーだけが2倍になる
func doubleByValue(n int) {
    n = n * 2
}

// 参照渡し風:nの住所(*int)を受け取り、元の値を2倍にする
func doubleByPointer(n *int) {
    *n = *n * 2
}

func main() {
    x := 10
    doubleByValue(x)
    fmt.Println("値渡しの後:", x) // 10 のまま

    doubleByPointer(&x)
    fmt.Println("参照渡し風の後:", x) // 20 に変わる
}

実行すると、値渡しの後はxがそのまま(コピーだけ変更されたため)、参照渡し風の後はxが変化します。これが「コピーを触るか、原本を触るか」の違いです。

2. Go言語は基本的に値渡し

2. Go言語は基本的に値渡し
2. Go言語は基本的に値渡し

Go言語では、関数に渡す引数は必ず値渡しです。つまり、呼び出し時にその型の値のコピーが作られ、関数の中で操作されます。元の変数そのものを直接いじるわけではないので、関数内の変更が自動的に外へ伝わることはありません。

コピーといっても大きさは型ごとに異なります。intなら数値1個、[3]intのような配列は要素を含む全体がコピーされます。大きなデータを何度も渡すときは、このコピーが処理時間やメモリに影響することがある点を覚えておきましょう。

まずは超シンプルな確認(整数と配列)


package main

import "fmt"

// 値渡し:nの「コピー」を受け取り変更するだけ
func touchInt(n int) {
    n = 99
}

// 値渡し:配列全体の「コピー」を受け取る
func touchArray(a [3]int) {
    a[0] = 99
}

func main() {
    x := 10
    touchInt(x)
    fmt.Println("touchInt後:", x) // 10 のまま(元は変わらない)

    arr := [3]int{1, 2, 3}
    touchArray(arr)
    fmt.Println("touchArray後:", arr) // [1 2 3] のまま(元は変わらない)
}

どちらの例でも、関数内で値は変えているのに、関数の外では元の変数がそのままです。これは、渡されたのがコピーだから。コピーをいくら書き換えても、原本には影響しない――これがGoの値渡しの基本的な考え方です。

3. スライスは「構造体」のコピーが渡される

3. スライスは「構造体」のコピーが渡される
3. スライスは「構造体」のコピーが渡される

スライスは内部的に3つの情報(ヘッダ)を持つ小さな構造体です。これは「どの配列を見ているか」を示す先頭アドレス、要素数を表す長さ(len)、領域の余裕を示す容量(cap)のセットです。関数にスライスを渡すと、このヘッダのコピーだけが渡され、中身の配列(実データ)は共有されたままになります。

  • 先頭アドレス:実データ(配列)の入口を指す
  • 長さ(len):今見えている要素数
  • 容量(cap):後ろにどれだけ余裕があるか

イメージとしては「地図の写し」を渡しているだけで、街そのもの(配列)は一つ。地図の写しを折りたたっても、街の大きさは変わりません。

ヘッダはコピー、配列は共有されることを覗いてみる


package main

import "fmt"

func peek(s []int) {
    fmt.Printf("関数内: len=%d cap=%d first=%p\n", len(s), cap(s), &s[0])
    // ヘッダ(長さ)だけを変更しても、呼び出し元の長さは変わらない
    s = s[:1]
    fmt.Printf("関数内(切り直し後): len=%d cap=%d first=%p\n", len(s), cap(s), &s[0])
}

func main() {
    nums := []int{10, 20, 30}
    fmt.Printf("関数外: len=%d cap=%d first=%p\n", len(nums), cap(nums), &nums[0])

    peek(nums)

    // 呼び出し後も、ヘッダ(len/cap)は呼び出し元側ではそのまま
    fmt.Printf("関数外(呼び出し後): len=%d cap=%d first=%p\n", len(nums), cap(nums), &nums[0])
}

出力を見ると、first(先頭要素のアドレス)は関数内外で同じ=同じ配列を見ていることが分かります。一方で、関数内でスライスを「切り直し」て長さを変えても、呼び出し元の長さは変わりません。これは、渡されているのが「ヘッダのコピー」だからです。

4. そのため関数内でスライスの中身を変更すると元も変わる

4. そのため関数内でスライスの中身を変更すると元も変わる
4. そのため関数内でスライスの中身を変更すると元も変わる

構造体の中の「配列の先頭アドレス」は同じなので、関数内でスライスの中身(配列要素)を変更すると元の配列データも変わります

つまり、スライスの中身は参照渡しに近い動きになります。

ただし、スライス自体の長さや容量を変更しても、コピーした構造体だけが変わるので、元のスライスの長さや容量は変わりません。

5. 実際にコードで確認しよう

5. 実際にコードで確認しよう
5. 実際にコードで確認しよう

package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 100         // スライスの中身を変更
    s = append(s, 200) // スライスの長さを変更(コピーにのみ影響)
    fmt.Println("関数内:", s)
}

func main() {
    nums := []int{1, 2, 3}
    modifySlice(nums)
    fmt.Println("関数外:", nums)
}

このコードの実行結果は次のようになります。


関数内: [100 2 3 200]
関数外: [100 2 3]

解説すると、関数内でスライスの中身(1番目の要素)が変更され、元のスライスにも反映されています。

しかし、appendでスライスを拡張した操作は関数内のコピーだけに影響し、元のスライスには反映されていません。

6. スライスを関数内で完全に変更したい場合はポインタを使う

6. スライスを関数内で完全に変更したい場合はポインタを使う
6. スライスを関数内で完全に変更したい場合はポインタを使う

もし関数内でスライス自体(長さや容量)も元の変数に反映させたい場合は、スライスのポインタ(*[]int)を渡す方法があります。


func modifySlicePointer(s *[]int) {
    *s = append(*s, 300)
    fmt.Println("関数内:", *s)
}

func main() {
    nums := []int{1, 2, 3}
    modifySlicePointer(&nums)
    fmt.Println("関数外:", nums)
}

この場合、関数内でスライスの長さを変える操作も、元の変数に反映されます。

7. スライスの動きを理解すれば思い通りに扱える

7. スライスの動きを理解すれば思い通りに扱える
7. スライスの動きを理解すれば思い通りに扱える

Go言語のスライスを関数に渡すときは、構造体のコピーが渡されますが、中身の配列データは共有されます。

そのため、関数内でスライスの中身を変更すると元のデータも変わりますが、スライスの長さや容量の変更はコピーのみに影響します。

スライス自体を関数内で完全に変更したい場合は、ポインタを使って渡す方法を選びましょう。

このしくみを理解すると、Go言語で安全かつ効率的にスライスを扱えるようになります。

まとめ

まとめ
まとめ

Go言語でスライスを関数に渡すとき、見た目は値渡しでも中身の配列は共有される仕組みがあるため、少し混乱しやすいところです。今回の内容を振り返ると、スライスは「配列の先頭アドレス」「長さ」「容量」を持つ特別な構造体で、その構造体だけがコピーされ、実際の配列データは共有されます。つまり、関数内で値を変更したとき、元のデータにも反映されるという特徴があるので、意図せずデータが変わってしまうときは注意が必要になります。 また、スライスの長さや容量を変更すると、コピー側だけが大きくなり、元のスライスには反映されません。このような動きを理解しておくと、データ処理や大規模な処理でも混乱せずに進めることができます。 実務やチームでの開発でも、スライスの動きは重要なポイントとしてよく話題に上がります。配列との違いや、なぜ参照のように動くのかをしっかり知っておくことで、予期しないバグの防止にもつながります。毎回新しい変数を作る必要があるのか、ポインタを使うべきかなど、状況に応じて選べるようになるとより柔軟です。 さらに、関数内でスライスそのものを更新する必要がある場合には、ポインタを使うという選択肢が生まれます。ポインタで渡すことによって、スライスの中身だけでなく長さや容量を増やした場合も、元の変数に反映できるので、処理が複雑な場面でも役立ちます。スライスは配列より扱いやすく、可変で使いやすいデータ構造なので、Go言語のコードでは頻繁に登場します。そのため、今回の内容をしっかり理解することは、Go言語を扱ううえでとても大切な土台になります。 実際のプログラム例を交えて振り返りながら、スライスがどのように動くのかを再確認してみましょう。

スライス操作のサンプルプログラム例

こちらは、スライスを受け取る関数と、変更結果を確認するためのプログラムです。関数内での変更がどこまで外側に影響するのかを比較できます。


package main

import "fmt"

func changeSlice(s []int) {
    s[1] = 50
    s = append(s, 999)
    fmt.Println("関数内:", s)
}

func main() {
    numbers := []int{10, 20, 30}
    changeSlice(numbers)
    fmt.Println("関数外:", numbers)
}

このプログラムでは、関数内で二番目の要素を変更しています。その変更は元のスライスにも反映されています。ところが、appendで値を追加した後の長さは、関数外のスライスには反映されません。スライスの中身は共有されるのに、長さは共有されない。この少し不思議な動きこそが、スライスを理解する大切なポイントになります。 実践的な場面でも、データの加工やフィルタリングでスライスはよく使用されるので、意図せず元のデータが書き変わらないように注意したり、逆に意図して変更を反映させたいときにはポインタを使ったりと、柔軟に使い分ける力が求められます。

ポインタで渡すサンプルコード

今度はスライスのポインタを渡すパターンです。関数内でスライスに要素を追加して、元の変数にも反映される様子を確認できます。


func changePointerSlice(s *[]int) {
    *s = append(*s, 777, 888)
    fmt.Println("関数内:", *s)
}

func main() {
    data := []int{5, 6, 7}
    changePointerSlice(&data)
    fmt.Println("関数外:", data)
}

この例では、appendの結果がそのまま元のスライスに反映されます。ポインタを使うことで、関数の外側にあるスライスをそのまま操作できるため、データ処理全体を見渡しながら安心して書き進めることができます。特に大量のデータを扱う場面や、複数の関数で同じスライスを扱う場面では便利です。

先生と生徒の振り返り会話

生徒

「スライスって値渡しなのに中身だけ変わるって不思議でした。でも構造体がコピーされるって考えたら納得できました。」

先生

「そうですね。中身の配列は同じ場所を見ているので、中身だけ書き変わるという特徴があります。実際に触りながら覚えると理解が深まりやすいですよ。」

生徒

「ポインタで渡すとスライスそのものも変わるんですね。状況によって使い分けるのが大切なんだとわかりました。」

先生

「その通りです。スライスは便利なので慣れるほど扱いやすくなりますよ。」

Go言語を基礎からスッキリ学びたい人や、 文法だけでなく「実用的な使い方」まで押さえたい人には、 定番の入門書がこちらです。

基礎からわかるGo言語をAmazonで見る

※ Amazon広告リンク

この記事を読んだ人からの質問

この記事を読んだ人からの質問
この記事を読んだ人からの質問

プログラミング初心者からのよくある疑問/質問を解決します

Go言語のスライスを引数に渡すときに値渡しと参照渡しの違いがわからないのですが、簡単に理解する方法はありますか?

Go言語のスライスは値渡しでも構造体だけがコピーされるため、中身の配列データは共有されます。そのため関数内で配列要素を変更すると元のデータも変わります。配列全体が複製されるわけではなく、先頭アドレスと長さと容量の構造が複製されるだけなので、見た目は値渡しでも動きは参照渡しに似ています。
関連セミナーのご案内

【超入門】ゼロから始めるGo言語プログラミング:最速で「動くアプリ」を作るマンツーマン指導

「プログラミングの仕組み」が根本からわかる。Go言語でバックエンド開発の第一歩を。

本講座を受講することで、単なる文法の暗記ではなく、「プログラムがコンピュータの中でどう動いているか」という本質的な理解につながります。シンプルながら強力なGo言語(Golang)を通じて、現代のバックエンドエンジニアに求められる基礎体力を最短距離で身につけます。

具体的な開発内容と環境

【つくるもの】
ターミナル(黒い画面)上で動作する「対話型計算プログラム」や、データを整理して表示する「ミニ・ツール」をゼロから作成します。自分の書いたコードが形になる感動を体験してください。

【開発環境】
プロの現場でシェアNo.1のVisual Studio Code (VS Code)を使用します。インストールから日本語化、Go言語用の拡張機能設定まで、現場基準の環境を一緒に構築します。

この60分で得られる3つの理解

1. 環境構築の完全な理解

「なぜ動くのか」という設定の仕組みを理解し、今後の独学で詰まらない土台を作ります。

2. Go言語の基本構造(変数・型)

データの種類やメモリの概念など、他言語にも通じるプログラミングの本質を学びます。

3. 読みやすいコードの書き方

ただ動くだけでなく、誰が見ても分かりやすい「綺麗なコード」を書くための考え方を伝授します。

※本講座は、将来的にバックエンドエンジニアクラウドインフラに興味がある未経験者のためのエントリー講座です。マンツーマン形式により、あなたの理解度に合わせて進行します。

セミナー画像

初めてのGo言語を一緒に学びましょう!

カテゴリの一覧へ
新着記事
New1
Go言語
Go言語のwhile的なforループの使い方!条件式ループの基本を解説
New2
Go言語
Go言語プログラムの実行方法まとめ!VSCode・ターミナルでの実行手順を解説
New3
Swift
Swift意味とは?プログラミング言語・金融・鳥の違いを徹底解説
New4
Swift
Swift 戻り値の扱い方と複数戻り値の返し方|初心者でも分かる関数の基本
人気記事
No.1
Java&Spring記事人気No1
Go言語
Go言語でリダイレクト処理を行う方法(http.Redirect)を初心者向けに解説
No.2
Java&Spring記事人気No2
Swift
Swift開発環境の構築方法を徹底解説!Xcode・Windows・Linux対応
No.3
Java&Spring記事人気No3
Kotlin
Android Studioのインストール手順と初期設定を初心者向けに完全解説!
No.4
Java&Spring記事人気No4
Kotlin
Gradleファイル(build.gradle.kts)の書き方と役割をやさしく解説!Kotlin初心者向け完全ガイド
No.5
Java&Spring記事人気No5
Go言語
Go言語のgo.modファイル完全ガイド!初心者でもわかる仕組みと書き方
No.6
Java&Spring記事人気No6
Swift
Swift Playgroundの使い方を完全解説!初心者に最適な学習環境の始め方
No.7
Java&Spring記事人気No7
Kotlin
Kotlinの演算子一覧と使い方!算術・比較・論理演算子の基本を解説
No.8
Java&Spring記事人気No8
Go言語
Go言語で条件式を1行で書くコツ!三項演算子の代替と短縮記法