LoginSignup
7

More than 5 years have passed since last update.

Go初学者が学んだことまとめ〜その2〜(参照型)

Last updated at Posted at 2018-09-20

はじめに

こちらの続きです。
Go初学者が学んだことまとめ〜その1〜(構成と実行、構文)

今回は参照型についてです。

参照型

スライス

定義

可変長配列を表現する型。
下記のように定義できる。

var s []int
s := make([]int, 5) //[0, 0, 0, 0, 0]
s := []int{1, 2, 3} //[1, 2, 3]

要素数と容量

スライスには要素数と容量がある。
要素数はlen,容量はcapで取得できる。

//makeの第2引数が要素数、第3引数が容量
s := make([]int, 5, 10)
fmt.Println(len(s)) //5
fmt.Println(cap(s)) //10

容量とは、メモリ上に確保する領域のこと。
要素数を拡張していく際に、新たな領域の確保が不要になるというメリットがある。

append

配列の拡張ができる。

s := []int{1, 2, 3} 
s = append(s, 4) // s == [1, 2, 3, 4]
s = append(s, 5, 6, 7) // s == [1, 2, 3, 4, 5, 6, 7]

拡張される容量は、要素数と同じではない。
ランタイムが自動で拡張している。
どの程度拡張されるかは、Goのランタイムに依存する。

s := []int{1, 2, 3}
fmt.Println(cap(s)) //3
s = append(s, 4)
fmt.Println(cap(s)) //6
s = append(s, 5, 6, 7)
fmt.Println(cap(s)) //12

appendした時は、自動拡張するかどうかわからないということになる。
つまりこういうこと。

s1 := append(s0, x) //s0とs1が同じメモリ領域であるかは不明

なので、自動拡張の有無を意識しないといけないコードは基本NG。

簡易スライス式

配列やスライスから、新たなスライスを生成する。
インデックスの範囲を[m:n]の形式で指定する。

a := [5]int{1, 2, 3 ,4, 5}
s := a[0:2] //s == [1, 2] sは[]int型

完全スライス式

3つのパラメータを取る記法。

a[low:high:max] //0 <= low <= high <= max <= cap(a) という関係を満たす必要がある

maxという値で容量をコントロールできる。

a := [10]int{1, 2, 3 ,4, 5, 6, 7, 8, 9, 10}
s1 := a[2:4]
fmt.Println(cap(s1)) // 8 //len(a)-low が容量になる

s2 := a[2:4:4]
fmt.Println(cap(s2)) // 2 //max-low が容量になる

s3 := a[2:4:6]
fmt.Println(cap(s3)) // 4 //max-low が容量になる

「容量」という概念が非常にややこしいので注意。

配列とスライスの引数

スライスは「参照型」であるので、スライスの配列要素の参照(ポインタ)を渡すことになる(詳細は後述)。
それに対して配列は、関数側に配列のコピーされたものが渡される。
下記は動作の違いを示したもの。

Go
func powArray(a [3]int) {
    for i, v := range a {
        a[i] = v * v
    }
}

func powSlice(s []int) {
    for i, v := range s {
        s[i] = v * v
    }
}

func main() {
    a := [3]int{1, 2, 3} 
    powArray(a) //コピーされたものが関数に渡るのでaは変更されない。
    fmt.Println(a) //[1, 2, 3]

    s := []int{1, 2, 3}
    powSlice(s) //関数はsの配列要素と同一のメモリ領域を使用するためsは変更される。
    fmt.Println(s) //[1, 4, 9]
}

ちなみに、JavaScript(TypeScript)の配列は参照渡し。
プリミティブ型(number,string,boolean)は値渡し、それ以外は参照渡しになる。

TypeScript
function pow(a: number[]): void {
    for(let i = 0; i < a.length; i++) {
        a[i] = a[i] * a[i];
    }
}
const ary = [1, 2, 3];
pow(ary);
console.log(ary); //[1, 4, 9];

さらに深堀すると・・・

スライスの内部構造は

  • 配列へのポインタ
  • 要素数 (length)
  • 容量 (capacity)

の3つからなる。
関数の引数にスライスを渡した場合、上記の3つが値渡しされている。
下記はそれを示す例。

func add(s []int) {
    s = append(s, 1)       // s => ptr = 0xc0000180a0, len = 1, cap = 10
    fmt.Printf("%p\n", s)  // 0xc0000180a0 配列のアドレスは呼び出し元と同じ
    fmt.Printf("%p\n", &s) // 0xc00000a0a0 スライス自体のアドレスは呼び出し元と違う
    fmt.Println(s) //[1]
}

func main() {
    s := make([]int, 0, 10) // s => ptr = 0xc0000180a0, len = 0, cap = 10
    fmt.Printf("%p\n", s)   // 0xc0000180a0 配列のアドレス
    fmt.Printf("%p\n", &s)  // 0xc00000a060 スライス自体のアドレス
    add(s)
    fmt.Println(s) // [] len = 0なので出力は[]になる
}

appendすると参照元のスライスは書き換わるように見えるが、実際の出力は書き換わっていないように見える。
これは、スライスの要素数は値渡しのため、引き渡した関数側でappendして要素数を書き換えても、参照元には反映されないためである。

マップ

定義

連想配列を表現する型。
下記のように定義できる。

var m map[int]string
m := make(map[int]string)
m := map[int]string{1: "Taro", 2: "Hanako", 3: "Jiro"}

特別な使い方はないので、省略。

チャネル

ゴルーチンとゴルーチンの間でデータを受け渡しするためにデザインされたGo特有のデータ構造。

定義

var ch chan int     //双方向
var ch1 <-chan int  //受信専用
var ch2 chan<- int  //送信専用
ch := make(chan int, 8) //バッファサイズ8

チャネルはFIFO(先入先出し)のキュー。
バッファサイズとはこのキューのサイズ。

使い方

チャネルは必ずゴルーチンと一緒に使う。
下記は0〜10000をゴルーチン間で受け渡し、出力するプログラム。

package main

import "fmt"

func receiver(ch <-chan int) {
    for {
        i, ok := <-ch
        if ok == false {
            //受信できなくなったら終了
            break
        }
        fmt.Println(i)
    }
}

func main() {
    ch := make(chan int)
    go receiver(ch)

    i := 0
    for i <= 10000 {
        ch <- i
        i++
    }
    //closeするとi, ok := <-chのokがfalseになる
    close(ch)
}

select

複数のチャネルをコントロールする構文。
例えば

e1 := <- ch
e2 := <- ch

というコードで、変数ch1のチャネルからデータが受信できない場合、このゴルーチンは停止したままになり、ch2はいつまでたっても受信できない。
このように、1つの処理で複数のチャネルを処理できない問題に対処できるのがselect。
ゴルーチンを停止させることなく、全ての処理を継続させることができる。
このように書く。

package main

import "fmt"

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    ch3 := make(chan int)

    go func() {
        for {
            i := <- ch1
            ch2 <- (i * 2)
        }
    }()

    go func() {
        for {
            i := <- ch2
            ch3 <- (i - 1)
        }
    }()

    n := 1
LOOP:
    for {
        //処理が継続できるcaseの中からランダムに選択される
        //全て処理が継続できない場合はdefaultが選択される
        select {
        case ch1 <- n:
            n++
        case i := <- ch3:
            fmt.Println("received", i)
        default:
            if n > 100 {
                break LOOP
            }
        }
    }
}

非同期処理のデータ送受信がここまで簡単にできるのがGoの特徴。

Goは全て値渡し

When are function parameters passed by value?

everything in Go is passed by value

公式がおっしゃる通り、Goは全て値渡しとのことです。
Goで参照渡しという言葉を使うのは誤解を招くのでやめましょう。
(この記事も当初は参照渡しという言葉を使っていましたが、ご指摘により変更しました。)

まとめ

参照型についてまとめました。
スライスや、チャネルを使ったselectなどは少し難易度が上がりました。
selectをきちんと理解できれば、Go初心者は卒業できるのかなと思います。
その3に続きます。

参考

スターティングGo言語

関連

Go初学者が学んだことまとめ〜その1〜(構成と実行、構文)
Go初学者が学んだことまとめ〜その3〜(構造体、インターフェース)
Go初学者が学んだことまとめ〜その4〜(コマンド、パッケージ)

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
7