😸

Go 型・関数

2023/08/17に公開

<組み込み型>

  • 整数 :
    int
    byte(int8 のエイリアス)
    rune(int32 のエイリアス)
    int64
    uint(負数以外(0 以上)を扱う) など
  • 浮動小数点数 : float32, float64
  • 文字列 : string
  • 真偽値 : bool

<型変換(キャスト)>

基本は、strconvパッケージを使う。

ただし、数値同士のキャストはT(v)でできる。

T(v)
var f float64 = 10
var n int = int(f)

よく使うもの

  • Atoi : 文字列 → int(32 か 64 なのかは環境依存)
  • Itoa : int → 文字列
Atoi, Itoa
i, _ := strconv.Atoi("-42") // Atoi は返り値が2つあり、2つ目は使わないため、 _ で、これから使わないことを明示。
s := strconv.Itoa(-42)

あまり使わないもの

  • ParseX : 文字列 → X
    • ParseInt : 文字列 → int(指定の bit 値への変換)
    • ParseFloat : 文字列 → float
    • ParseUint : 文字列 → uint
    • ParseBool : 文字列 → bool
  • FormatX : X → 文字列
    • FormatInt : int8 など(int が指す bit 値ではない bit 値) → 文字列
    • FormatUint : uint → 文字列
    • FormatFloat : float → 文字列
    • FormatBool : bool → 文字列

<型の確認>

型の確認は、reflect.TypeOf() でできる。

fmt.Println( reflect.TypeOf(v) )

<コンポジット型>

複合データ型。構造体、配列、スライス、マップ がある。

構造体

型の異なるデータを集めたデータ型。

  • 初期化方法 : struct { name string, age int }
  • 操作 : 参照も代入も、.でアクセスできる。
    var person struct {
      name string
      age  int
    }
    // .でアクセス
    person.name = "太郎"
    fmt.Println(person.name) // 太郎
    

配列

同じ型のデータを集めたデータ型。
要素数は変更できない

  • 初期化方法
    • [2]string{"hoge", "fuga"}
    • [...]string{"hoge", "fuga"}
    • [...]int{2: 10, 5: 20}
      arr[i]にあたる位置に値を指定する方法。この例だと[0 0 10 0 0 20]となる。
  • 操作
    • アクセス : arr[3]
    • 長さ : len(arr)
    • スライス演算 : arr[2:5]
      配列からスライスを作る。この例だとarr[2] から arr[5-1] までを抽出したものになる。
      スライス演算
      arr := [...]string{"a", "b", "c", "d", "e", "f"}
      fmt.Println(arr[2:5]) // [c d e]
      
2 次元配列
2次元配列
func main() {
  var twoD [3][5]int

  for i := 0; i < 3; i++ {
    for j := 0; j < 5; j++ {
      twoD[i][j] = (i + 1) * (j + 1)
    }
    fmt.Println("Row", i, ":", twoD[i])
  }
  fmt.Println(twoD)
}
/* 出力
  Row 0 : [1 2 3 4 5]
  Row 1 : [2 4 6 8 10]
  Row 2 : [3 6 9 12 15]
  [[1 2 3 4 5] [2 4 6 8 10] [3 6 9 12 15]]
/*

スライス

配列の一部を切り出したデータ型。
(スライスは配列を切り出したものなので、背後には必ず配列が存在する。)

配列との大きな違いは、スライスの長さ(サイズ)が固定ではなく動的であること。

スライスは下記 3 つの要素を持っている。(この 3 要素の構造体である。)

  1. ポインタ : 基となる配列の最初の要素の場所切り出した場所)を表す。
  2. len : 切り出した長さ。
  3. cap : 基になる配列に対して、ポインタの位置(切り出した位置)から配列の終端までの要素数。つまり、このスライスはどこまで拡張できるのかを表す量。
  • 初期化方法
    • arr[2:5]
      スライス演算子s[i:p]によって、(既にある)配列から切り出す方法。
      この例だとarr[2] から arr[5-1] までを抽出したものになる。
      i と p は省略することもでき、配列の全要素をベースにする場合はarr[:]となる。
    • []int{1, 2, 3}
      スライスリテラルで、基になる配列は指定せずにスライスだけを作る方法。
      (正しくは、内部的に{1, 2, 3}の配列が自動で作られ、それをスライスが参照している。)
    • []int{2: 10, 5: 20}
      slice[i]にあたる位置に値を指定する方法。この例だと[0 0 10 0 0 20]となる。
    • make([]int, 3, 10)
      make メソッドによって指定した要素数、容量のスライスを作る方法。make([]型名, length, capacity)
  • 操作
    • アクセス : sl[3]
    • 長さ : len(sl)
    • 容量 : cap(sl)
    • 要素の追加 : sl = append(sl, 50, 60)
    • 要素の削除
      下記のどちらも、(削除したい要素を除外した)削除しない側の 2 つの要素郡をくっつけることで、要素を削除したスライスを作成している。
      • i 番目の要素のみ削除 : a = append(a[:i], a[i+1]...)
      • i~j 番目の要素を削除 : a = append(a[:i], a[j]...)

空のスライスを作る方法

p := []byte{}

配列をコピーして使う方法

copy(移す先のスライス, 元の配列から欲しい要素のスライス)を使う。

func main() {
  // 配列
  letters := [...]string{"A", "B", "C", "D", "E"}
  fmt.Println("Before", letters) // Before [A B C D E]

  slice1 := letters[0:2] // A, B

  slice2 := make([]string, 3) // cap=3と指定した 空のスライスが生成される
  // copy!!
  copy(slice2, letters[1:4]) // slice2にletters[1:4]をコピーする -> B, C, D

  slice1[1] = "X"

  fmt.Println("slice1", slice1) // slice1 [A X]
  fmt.Println("slice2", slice2) // slice2 [B C D] : 要素が変更されていない!

  fmt.Println("After", letters) // After [A X C D E] : slice1が基にしている配列は当然変更される
}

マップ

キーと値をマッピングしたデータ型。

  • 初期化方法
    • map[string]int{ "hoge": 1, "fuga": 2 }
    • make(map[string]int)
  • 操作
    • アクセス : m["x"]
    • 存在確認 : n, ok := m["z"] で、n には値、ok には bool が入る。
    • 追加 : m["z"] = 30
    • 削除 : delete(m, "z")

<ユーザ定義型>

type 型名 基底型 で定義する。(基底型 : 基にする型。)

なので、ユーザ定義型というのは struct だけでなく、type MyInt intとかも定義できる。

関数

  • 引数の型は、変数名の後ろに書く。
  • 2 つ以上の引数の型が同じ場合は、1 つに省略できる。
  • 複数の戻り値を返せる。
    使わない戻り値は、ブランク変数_によって明示すること。Go の変数は必ず使わないといけないため。
func sample(x, y int) (string, string) {
  // 処理
  // return ...
}

Named return value

返り値の定義で変数を定義できる。
それで、それをそのまま関数内で使えるのはもちろん、return とするだけでその変数が返される。

func greetingPrefix(language string) (prefix string) {
	switch language {
	case japanese:
		prefix = japaneseHelloPrefix
	case french:
		prefix = frenchHelloPrefix
	default:
		prefix = englishHelloPrefix
	}
	return
}

https://zenn.dev/yuyu_hf/articles/c7ab8e435509d2

<ポインター>

ポインターじゃないとどうなるか

多くの言語と同じように、Go は「値渡し」の言語。
つまり、b := aでは(変数 b に変数 a のメモリ位置を共有している訳でなく、)変数 a の値のコピーを変数 b に格納している。
ローカルコピー(メモリ内の新しい変数)を作成している。

よって、関数の引数で変数(値)を渡しても、その関数内での変更は呼び出し元に反映されない
その引数は、元の変数の値のコピーなので。

関数に変数を渡しても呼び出し元に反映されない
func main() {
  first := "ジョン"
  updateName(first)
  println(first) // ジョン と出力される
}

func updateName(name string) {
  name = "田中" // "田中"に書き換えたつもりだが...
}

ポインターの用途

ただ、そうじゃなくて「関数内で値を変更したい」というときにポインタを使う。
ポインターでは、値ではなくアドレスメモリを渡す。そうすることで、呼び出し元にも反映される
(ポインターを使うことで、上記の updateName 関数で行う変更を main 関数の first 変数にも反映させることができる。)

ポインターは、変数の格納先(メモリアドレス)を表す値

  • &演算子 : ポインタを渡す
  • *演算子 : ポインターを参照して、(ポインターが示す)格納先のオブジェクトへアクセスする。
main.go
func main() {
  first := "ジョン"
  updateName(&first) // ポインター(メモリアドレス)を渡す
  println(first) // 田中 と出力される
}

func updateName(name *string) { // 注: 変数名でなく、型のとなりに*を書く
  *name = "田中" // ポインター先の文字列をupdate
}

<メソッド>

メソッドは、レシーバ(= 構造体 や その他何かのデータ)に紐付けられた関数
メソッドによって、作成した構造体やデータに動作を追加できる。

レシーバは、第 0 引数みたいなイメージ。(なので変数の値のコピーが発生する。)

文法 : func (変数 レシーバ) メソッド名() 返却型 { ... }

type triangle struct {
  size int
}

func (t triangle) perimeter() int {
  return t.size * 3
}

func main() {
  t := triangle{3}
  fmt.Println("この三角形の外周:", t.perimeter())
}

レシーバにポインターを使う

メソッドに、変数でなくポインターを渡すほうが良い場合がある。(変数のアドレスを参照する。)

  • メソッドで変数を更新する場合。
  • 引数が(データ容量として)大きすぎる場合。→ そのコピーを回避したい。
ポインタを使う
type triangle struct {
  size int
}

// ポインタを使わないメソッド
func (t triangle) perimeter() int {
  return t.size * 3
}

// ポインタを使うメソッド
func (t *triangle) doubleSize() {
  t.size *= 2 // return しない
}

func main() {
  t := triangle{3}
  t.doubleSize() // ポインタを参照して tが更新される

  fmt.Println("size:", t.size) // size: 6
  fmt.Println("perimeter:", t.perimeter()) // perimeter: 18
}

埋め込まれた構造体のメソッドを呼び出すことができる

埋め込まれた構造体のメソッドを呼び出す
type triangle struct {
  size int
}

type coloredTriangle struct {
  triangle // 構造体を埋め込む
  color string
}

func (t triangle) perimeter() int {
  return t.size * 3
}

func main() {
  t := coloredTriangle{triangle{3}, "blue"}
  fmt.Println("size:", t.size)
  // coloredTriangleが、triangleのメソッドも使える
  fmt.Println("perimeter:", t.triangle.perimeter())
  // fmt.Println("perimeter:", t.perimeter()) // ↑の書き方のシンタックスシュガー
}

また、埋め込んだ構造体のメソッドをオーバーロードすることもできる。
※ オーバーロード(多重定義) : 同じ名前の関数等を定義すること。

その場合、下記のように、埋め込んだ側・埋め込まれた側どちらのメソッドも使用することができる。

埋め込んだ構造体のメソッドをオーバーロード
type triangle struct {
  size int
}

func (t triangle) perimeter() int {
  return t.size * 3
}

type coloredTriangle struct {
  triangle
  color string
}

func (t coloredTriangle) perimeter() int {
  return t.size * 5
}

func main() {
  t := coloredTriangle{triangle{3}, "blue"}
  fmt.Println("size:", t.size)

  // coloredTriangleで、coloredTriangleのメソッドを使う
  fmt.Println("perimeter:", t.perimeter()) // perimeter: 15

  // coloredTriangleで、triangleのメソッドも使える
  fmt.Println("perimeter:", t.triangle.perimeter()) // perimeter: 9
}

Discussion