Go言語 基礎講座 #5 データ型

f:id:shinta2000ttt:20200722180639p:plain この記事は辞書のように使っていただければと思っています。

目次

データ型の分類

Goのデータ型は4種類に分類する事ができます。それは

  • 基本型
  • 合成型
  • 参照型
  • インターフェース型

の4つです。

基本型

基本型に属するのは整数型・浮動小数点数型・複素型・ブーリアン型・文字列型です。

整数型

符号付き整数

  • int (32bit または 64bit)
  • int8 : -128 ~ 127
  • int16 : -32768 ~ 32767
  • int32 : -2147483648 ~ 2147483647
  • int 64 : -9223372036854775808 ~ 9223372036854775807
  • rune : -2147483648 ~ 2147483647。int32のエイリアスで慣習的にUnicodeのコードポイントである事を示すために使用。

符号なし整数

  • uint (32bit または 64bit)
  • uint8 : 0 ~ 255
  • uint16 : 0 ~ 65535
  • uint32 : 0 ~ 4294967295
  • uint 64 : 0 ~ 18446744073709551615
  • byte : 0 ~ 255。unit8のエイリアスで、慣習的に数値・量ではなく生データである事を示すために使用。
  • uintptr : ポインタ値。GoプログラムがOSなどとやり取りする際の低レベルプログラミングなどのみで使用。

浮動小数点数

  • float32
  • float64

複素型

複素数を表現する型です。

  • complex64 : 虚数部と実数部をfloat32で表現。
  • complex128 : 虚数部と実数部をfloat64で表現。

ブーリアン

TrueかFalseのみの真偽値型です。

文字列型

文字列は不変なバイト列と定義されます。

  • string

文字列には2つの種類があります。

  • 文字列リテラル:ダブルクォーテーションで囲む。
  • 生文字列リテラル:バッククォーテーションで囲む。エスケープシーケンスは処理されず、改行を入れる事ができる。

文字列の長さを取得する

str1 := "ABCDE"
str2 := "あいうえお"
 
fmt.Println("str1のバイト数=", len(str1))  // str1のバイト数=5
fmt.Println("str2のバイト数=", len(str2))  // str2のバイト数=10

len()関数は文字列のバイト長を取得するので、上の例のように英数字の場合は文字数=バイト長になりますが、平仮名などの場合は文字数*2=バイト長になります。
純粋に文字数を取得したい場合は以下を使います。

import (
    "utf8"
)

str1 := "ABCDE"
str2 := "あいうえお"

fmt.Println("str1の文字数=", utf8.RuneCountInString(str1))  // str1の文字数=5
fmt.Println("str2の文字数=", utf8.RuneCountInString(str2))  // str2の文字数=5

utf8というパッケージのRuneCountInString()関数を使う事で純粋な文字数を取得する事ができます。

文字列を扱うための標準パッケージ

bytes

バイト型のスライス([]byte)を扱うための関数を提供しています。

  • Buffer型 : bytes.Buffer。bytesがバイトスライスを効率よく扱うために用意されているデータ型。
strconv

string convertの略で、整数型やブーリアン型などを文字列型に変換・逆変換する事ができます。

import (
    "strconv"
)

x, _ := strconv.ParseInt("12345", 10, 32)
fmt.Println("x =", x)  // x = 12345

ParseInt関数を使うと文字列→整数の変換ができます。第一引数が文字列で第二引数が基数(何進数か)、第三引数がビット数です。上の例の場合、32ビット10進数に変換しています。"ABC"のような数値に変換できない文字列を入力するともちろんエラーになります。

ちなみに戻り値の _ はGoでは使用しない戻り値などを明示的に示すための方法です。

x, err := strconv.Atoi("12345")
if err != nil {
    fmt.Println("変換エラー")
}
fmt.Println("x =", x)  // x = 12345

Atoi()関数は文字列→整数の別の変換方法です。これは文字列だけを入力すればよいのでシンプルです。

import (
     "reflect"
)

x := int64(12345)

y := strconv.FormatInt(x, 10)
fmt.Printf("y = %s, type = %s", y, reflect.TypeOf(y))  // y = 12345, type = string

整数→文字列の変換をするにはFormatInt()関数を使います。第一引数は数値で、第二引数は基数です。

fmt.Println("y =", strconv.Itoa(12345))  // y = 12345

Itoa()関数は整数→文字列の別の変換方法で非常にシンプルです。数値を入力するだけで文字列を返してくれます。

unicode

runeを分類や変換するための関数を提供してます。この文字は大文字なのかなどを判別できたりします。

合成型

合成型には配列や構造体があります。

配列

空配列

空(0)配列は以下のように定義します。

var array1[10] byte  // 整数(int8)配列
var array2[3][5] int  //多次元整数配列

fmt.Println("array1 =", array1)  // array1 = [0 0 0 0 0 0 0 0 0 0]  
fmt.Println("array2 =", array2)  // array2 = [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

var 配列名[配列の長さ] データ型 で定義すると、そのデータ型のその長さの空配列が生成されます。

参照・代入

var array[3] int

array[0] = 1
array[1] = 2
array[2] = 3

for _, value := range array{
    fmt.Println("value =", value)
}

/*
value = 1
value = 2
value = 3
*/

配列のあるインデックスの値を参照するには上の例のように行います。

初期化

配列の定義の時点で予め値で初期化する方法です。

array := [...]string {"あ", "い", "う", "え", "お"}
for _, value := range array{
    fmt.Println(value)
}
/*





*/

配列名 := [...]型 {初期値} と書きます。...と書くことで初期値の数に合わせて自動的に配列の長さを調節してくれます。
もう一つ初期化する方法があります。

var array [5]string = [5]string {"あ", "い", "う", "え", "お"}

for _, value := range array{
    fmt.Println(value)
}
/*





*/

これは配列の変数を定義し、そこに初期化した無名の配列を代入する形になっていあます。ここで注意しなければいけないのは、定義した配列の長さと、初期値が入った無名の配列の長さは一致させておく事です。

構造体

構造体を作成

Goにはオブジェクト指向言語のクラスなるものはありません。その代わりにデータやメソッドなどをひとまとめにする構造体というデータ型が用意されています。

var strct struct{
    x string
    y int
    z bool
}

strct.x = "ABCDE"
strct.y = 10
strct.z = true

fmt.Printf("x =%d, y =%s, z =%t", strct.x, strct.y, strct.z)  // x = ABCDE, y = 10, z = true

構造体はこのように作成します。struct{}で構造体を作成でき、それを変数strctに代入しています。中のxyzはフィールドと呼ばれデータを格納できます。フィールド名 型 のようにして定義します。
また構造体のフィールドの参照は、 構造体.フィールド名 で行います。

構造体の作成時に任意のフィールド値で初期化することも可能です。

var customer struct{
    Name string
    Age uint8
    Email string
}{
    Name: "田中浩"
    Age: 35
    Email : "hiroshi111@gmail.com"
}

このように構造体の後ろに{フィールド名: 値}という記法を追加することで初期化する事ができます。

type / 構造体を定義

type Student strunct{
    Name string
    Year uint8
    Club string
}

func StructureType() {
    var student1 Student

    student1.Name = "伊藤健二"
    student1.Year = 3
    student1.Club = "サッカー"
    fmt.Println(student1)  // {伊藤健二 3 サッカー}
}

typeという予約語は既存の型や型リテラルに別名を付けて新しい型を定義できます。これを使って上の例では構造体からStudentという新しい型を作りました。そしてStudent型から構造体を作成してsetudent1に代入しています。
これでオブジェクト指向におけるクラスとインスタンスのような関係を再現できます。

student2 := Student{
    Name: "松島花子"
    Year: 2
    Club: "バレーボール"
}

fmt.Println(student2)  // {松島花子 2 バレーボール}

このようにtypeを使って定義した構造体でも値で初期化する事ができます。

構造体でJSONを扱う

GoでJSONを扱うには構造体を使い、JSONのデータをフィールドに格納していきます。

import (
    "json"
)

type TeacherJSON struct {
    Name string `json:"name"`
    Age uint8 `json:"age"`
    Address string `json:"address"`
}

func DealJson(){
    jsondata := []byte(`
    {
    "name": "岡崎一郎",
    "age": 45,
    "address": "東京都世田谷区〇〇一丁目"
    }
    `)

    var result TeacherJSON
    json.Unmarshal(jsondata, &result)
    fmt.Println(result)  // {岡崎一郎 45 東京都世田谷区〇〇一丁目}
}

&の使い方についてはポインタの項で説明します。
json.Unmarshal()関数を使う事で構造体にJSONデータを格納できます。ここで[]byte(データ)のようにかくとデータをバイナリデータ(生データ)に変換する事ができます。Unmarshal()関数の第一引数がbyte型でないといけないので変換します。

構造体の埋め込み

構造体の中に別の構造体を埋め込む事ができます。これによってオブジェクト指向におけるクラスの継承のような事が可能になります。

type EnbededAddress struct{
    Prefecture string
    City string
    Address string
}

type Teacher struct{
    Name string
    Age uint8
    EnbededAddress
}

func EnbededStructure() {
    var teacher1 Teacher
    teacher1.Name = "木下雅美"
    teacher1.Age = 27
    teacher1.Prefecture = "神奈川県"
    teacher1.City = "川崎市"
    teacher1.Address = "宮前区〇〇二丁目"
    fmt.Println(teacher1)  // {木下雅美 27 {神奈川県 川崎市 宮前区〇〇二丁目}}
}

方法は簡単で親となる上位構造体の定義の中で子となる下位構造体をフィールドとして指定するだけです。参照の際はいちいち下位構造体をしてしなくても、親構造体名.子構造体のフィールド名 で参照できます。

参照型

プログラム上の変数あるいは状態を間接的に参照するので参照型です。参照型にはポインタやスライス、マップ、チャネル、関数があります。

ポインタ

ポインタとは変数などのオブジェクトがメモリ上のどの位置に格納されているかという情報(アドレス)を参照・保持しているものです。 ポインタの詳しい説明はこちらから↓

https://wa3.i-3-i.info/word12814.html

Goでも変数のポインタを取得したり、ポインタが参照している変数の値を取得したりできます。

var ptr *int
x :=10
ptr = &x

fmt.Println("ポインタ変数ptrが指し示す変数xのアドレス =", ptr)  // ポインタ変数ptrが指し示す変数xのアドレス = 824634318856
fmt.Printf("x =%d, ポインタ変数ptrが指し示す変数xの値 =%d", x, *ptr)  // x = 10, ポインタ変数ptrが指し示す変数xの値 = 10

2つの記号を使います。*(アスタリスク)と&(アンパサンド)です。
ポインタ型を指定するにはvar 変数名 *データ型とする事で、「そのデータ型の変数を参照する」ポインタ変数を定義できます。

変数 → アドレス

変数からその変数のアドレスを取得するには&を使って&xです。

ポインタ変数 → 参照している変数の値

逆にポインタ変数から参照している変数の値を取得するには*を使って*ptrです。

func pls(x *int) {
    return *x + *x
}

x = 10
fmt.Println("x + x =", pls(&x))  // x + x = 20

もちろんこのように関数の引数にポインタを渡すこともできます。

スライス

配列が固定長なのに対し、スライス可変長でデータの出し入れが柔軟なデータ型です。

配列からスライス作成

array := [5]string {"あ", "い", "う", "え", "お"}
slice1 := array[:]  // 全ての要素を持ったスライス
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice1, len(slice1), cap(slice1))  // 参照範囲=["あ" "い" "う" "え" "お"], 長さ= 5, 容量= 5

slice2 := array[1:4]  // インデックスの1 ~ 3の要素を持ったスライス
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice2, len(slice2), cap(slice2))  // 参照範囲=["い" "う" "え"], 長さ= 3, 容量= 4

slice3 := array[:4]  // インデックスの0 ~ 3の要素を持ったスライス
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice3, len(slice3), cap(slice3))  // 参照範囲=["あ" "い" "う" "え"], 長さ= 4, 容量= 5

Goもインデックスは0から始まります。配列名[インデックス範囲] のように指定することでその範囲のスライスを作成できます。 [1:4]などと指定した場合、4までは参照されず4 - 1つまり3までが参照されます。

長さと容量

スライスは長さと容量という2つの数を持っています。長さとはスライスの要素数で、容量とはスライスの最初の要素から数えた、元の配列の要素数です。それぞれ組み込み関数のlen()とcap()で調べる事ができます。

ここでスライスに容量を超えて要素を追加してみます。

array := [5]string {"a", "b", "c", "d", "e"}
slice1 := array[:]

slice2 := append(slice1, "f")
slice3 := append(slice2, "g")

fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice2, len(slice2), cap(slice2))  // 参照範囲=["a" "b" "c" "d" "e" "f"], 長さ=6, 容量=10
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice3, len(slice3), cap(slice3))  // 参照範囲=["a" "b" "c" "d" "e" "f" "g"], 長さ=7, 容量=10

スライスへの値の追加は組み込み関数のappend()を使います。
ご覧のように容量を超えて要素を追加すると要領は元の2倍になります。
裏の動作としては新たにメモリ領域を拡張して全ての要素をそこへコピーし、appendされた分の要素も追加します。

スライスに別のスライスの要素を全て追加することもできます。

slice1 := make([]int, 3)

slice2 := make([]int, 2)
slice2 = append(slice2, 100)
slice2 = append(slice2, 200)

slice3 := append(slice1, slice2...)
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice3, len(slice3), cap(slice3))  // 参照範囲=[0 0 0 100 200], 長さ=5, 容量=6

append(追加するスライス, 追加されるスライス...) と書きます。

普通に作成・初期化

var slice[]int
slice = append(slice, 100)

fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d ¥n", slice, len(slice), cap(slice))  // 参照範囲=[100], 長さ=1, 容量=1

var 変数名 []型 で新しいスライスを作成できます。

slice := []int {10, 20, 30}

fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d ¥n", slice, len(slice), cap(slice))  // 参照範囲=[10 20 30], 長さ=3, 容量=3

また、[]型 {初期値} と書くと初期化することもできます。

make関数で作成

slice1 := make([]int, 3)
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice1, len(slice1), cap(slice1))  // 参照範囲=[0 0 0], 長さ=3, 容量=3

slice2 := make([]int, 3, 5)
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d", slice1, len(slice1), cap(slice1))  // 参照範囲=[0 0 0], 長さ=3, 容量=5

make関数で作成する場合、make([]型, 長さ, 容量) と書きます。なお容量を省略した場合は要領は長さと同じになります。
make関数ではスライスの他に、マップとチャネルも作成する事ができます。

スライスのコピー

slice1 := []int {100, 200}
slice2 := []int {10, 20 30}

count := copy(slice2, slice1)
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d ,コピー件数=%d", slice2, len(slice2), cap(slice2), count)  // 参照範囲=[100 200 30], 長さ=3, 容量=3,コピー件数=2

スライスに別のスライスの要素をコピーするにはcopy()関数を使います。第一引数がコピー先スライスを、第二引数がコピーされるスライスです。戻り値はコピー先スライスの要素のうち幾つがコピーによって変更されたかの数が帰ってきます。上の例の場合、インデックス0と1がコピーされた要素に置き換わったので、戻り値は2です。

slice1 := []int {100, 200}
slice2 := []int {10, 20 30}

count := copy(slice2[2:], slice1)
fmt.Printf("参照範囲=%v, 長さ=%d, 容量=%d ,コピー件数=%d", slice2, len(slice2), cap(slice2), count)  // 参照範囲=[10 20 100], 長さ=3, 容量=3,コピー件数=1

このようにコピー先のスライスを飛び出すように範囲を指定することもできますが、append関数のように容量が増えることはなく、飛び出した分は無視されます。

要素の削除

Goにはスライスから要素を削除する関数などは標準で組み込まれていません。そのため自分でその関数を作る必要があります。

func DeleteSlice(slice []int, i int) []int {
    slice := append(slice[:i], slice[i:]...)
    newSlice := make([]int, len(slice))
    
    copy(newSlice, slice)
    return newSlice
}

この関数は第一引数にスライスを、第二引数にインデックスをとります。そしてインデックス i - 1までと、i + 1からのスライスをくっ付けて新しいスライスに要素をコピーします。

slice := []int {1, 2, 3, 4, 5}
fmt.Printf("削除前:値=%v,長さ=%d,容量=%d", slice, len(slice), cap(slice))  // 削除前:値=[1 2 3 4 5],長さ=5,容量=5

slice = DeleteSlice(slice, 1)
fmt.Printf("削除後:値=%v,長さ=%d,容量=%d", slice, len(slice), cap(slice))  // // 削除後:値=[1 3 4 5],長さ=4,容量=4

ここでappend後のsliceをそのまま返すのではいけないのかという疑問があがるかもしれませんが、それをすると削除後のスライスの容量が4ではなく5のままになってしまい冗長なのです。

マップ

マップは配列やスライスと違い、キーを使ってデータ型を整理します。Pythonの辞書、Rubyのハッシュにあたります。

make関数で作成

map1 := make(map[int]string, 3)
map1[1] = "りんご"
map1[2] = "ゴリラ"
map1[3] = "ラッパ"

fmt.Printf("値=%v, 長さ=%d", map1, len(map1))  // 値=map[1:りんご 2:ゴリラ 3:ラッパ], 長さ=3

マップもmake関数で作成できます。その際はmake(map[キーの型]値の方, 容量) と書きます。なお容量の指定は省略できます。
値の参照は、マップ名[キー] で出来、代入も可能です。

初期化

var map2 = map[string]string{"名前": "まさお", "email": "masao123@gmail.com"}

map2["住所"] = "群馬県"
fmt.Printf("値=%v, 長さ=%d", map2, len(map2))  // 値=map[email:masao123@gmail.com 住所:群馬県 名前:まさお], 長さ=3

make関数を使わずとも、map[キーの型]値の型{初期値} と書くことでマップを初期化して作成できます。もちろんその後キーと値を追加することも可能です。

rangeにかける

マップをfor文の繰り返し処理でrangeにかけたときの挙動を説明します。

var students = map[int]string{1: "佐藤健吾", 2: "伊藤かなえ", 3: "下田良美"}

for key, value := range students{
    fmt.Printf("キー=%d, 値=%s", key, value)  
}

/*
キー = 3, 値 =下田良美
キー = 1, 値 = 佐藤健吾
キー = 2, 値 = 伊藤かなえ
*/

配列の場合、rangeはインデックスと要素を返していましたが、マップの場合はキーと値を返します。
また配列はインデックスで昇順にソートされているのに対し、マップはキーで値を識別するためソートがされていません。そのため上の例のようにfor文で取り出しても初期化した時とは違う順番で取り出されます。

キーの有無確認

teachers := make(map[int]string)
teachers[1] = "山田剛"
teachers[2] = "遠藤正樹"
teachers[3] = "加藤美紀"

value, exist := teachers[2]
if exist {
    fmt.Println(value)  // 遠藤正樹
}else {
    fmr.Println(exist)
}

マップ名[キー]で参照すると、valueの他にexistという値も得る事ができます。これはキー(と値)が存在するかの真偽値で、上の例の場合は存在しているのでtrueです。存在しない場合、valueは空に、existはfalseになります。

キー(と値)の削除

teachers := make(map[int]string)
teachers[1] = "山田剛"
teachers[2] = "遠藤正樹"
teachers[3] = "加藤美紀"

delete(teachers, 2)
fmt.Printf("値=%v,長さ=%d ¥", names, len(names))  // 値=map[1:山田剛 3:加藤美紀],長さ=2

キーと値を削除するにはdelete()関数が用意されています。delete(マップ名, キー) と書きます。

チャネル

チャネルは並行実行しているゴルーチン間を接続するパイプとして機能します。チャネルや並行処理、ゴルーチンは別記事で解説します。

関数

関数についても別記事で解説します。

インターフェース型

インターフェース型とは「メソッドにおける引数や戻り値の型だけを定義した」型で、オブジェクト指向でいうポリフォーリズムを実現する事ができます。こちらも別記事で解説します。