Go言語でのデータ型の理解

はじめに

データ型は、プログラムを書く際に特定の変数が格納する値の種類を指定します。データ型は、データに対してどのような操作が可能かも決定します。

この記事では、Goにネイティブな重要なデータ型について説明します。これはデータ型の完全な調査ではありませんが、Goで利用可能なオプションに慣れるのに役立ちます。基本的なデータ型を理解することで、効率的に実行される明確なコードを書くことができます。

背景

データ型について考える一つの方法は、現実世界で使用するさまざまな種類のデータを考えることです。現実世界のデータの例としては、数字があります:整数(0, 1, 2, …)、整数(…, -1, 0, 1, …)、無理数(π)などがあります。

通常、数学では異なる種類の数値を組み合わせて何らかの答えを得ることができます。例えば、5をπに加えたい場合があります:

5 + π

無理数を考慮して答えとして方程式を維持するか、πを少数点以下の桁数が短縮された数値に丸めてから数値を加算することができます:

5 + π = 5 + 3.14 = 8.14 

しかし、もし数字を他のデータ型、例えば単語で評価しようとすると、意味があまり通らなくなります。次のような方程式をどのように解くのでしょうか?

shark + 8

コンピュータにとって、それぞれのデータ型は非常に異なります—例えば単語と数字のように。その結果、値を割り当てるためにさまざまなデータ型をどのように使用するか、そして操作を通じてそれらをどのように操作するかに注意する必要があります。

整数

数学と同様に、コンピュータプログラミングにおける整数は正、負、または0の全体数です(…, -1, 0, 1, …)。Go言語では、整数はintとして知られています。他のプログラミング言語と同様に、4桁以上の数字にカンマを使用してはいけません。そのため、プログラム内で1,000と書く場合は、1000と書くべきです。

整数を次のように簡単に出力することができます:

fmt.Println(-459)
Output
-459

または、変数を宣言することもできます。この場合、変数は使用または操作している数字のシンボルです:

var absoluteZero int = -459
fmt.Println(absoluteZero)
Output
-459

整数を使ってGoで数学的な操作を行うこともできます。次のコードブロックでは、:=代入演算子を使用して変数sumを宣言し、インスタンス化します:

sum := 116 - 68
fmt.Println(sum)
Output
48

出力が示すように、数学的演算子-は整数68116から引き、結果として48を得ました。変数の宣言については、変数のデータ型の宣言のセクションで詳しく学びます。

Goプログラム内で整数は多くの用途で使用されます。Goについて学び続ける中で、整数を扱い、このデータ型に関する知識をさらに深める機会がたくさんあります。

浮動小数点数

浮動小数点数またはfloatは、整数で表現できない実数を表すために使用されます。実数にはすべての有理数と無理数が含まれるため、浮動小数点数は9.0や-116.42のような小数部分を含むことができます。Goプログラムでfloatを考える際には、小数点を含む数値として捉えることができます。

整数の場合と同様に、浮動小数点数を次のように簡単に出力することができます:

fmt.Println(-459.67)
Output
-459.67

また、次のように浮動小数点数を表す変数を宣言することもできます:

absoluteZero := -459.67
fmt.Println(absoluteZero)
Output
-459.67

整数と同様に、Goでも浮動小数点数を使って数学的な計算を行うことができます:

var sum = 564.0 + 365.24
fmt.Println(sum)
Output
929.24

整数と浮動小数点数では、3 ≠ 3.0であることに注意が必要です。3は整数を指すのに対し、3.0は浮動小数点数を指します。

数値型のサイズ

整数と浮動小数点数の区別に加えて、Goにはサイズの静的または動的な性質によって区別される2種類の数値データ型があります。最初のタイプはアーキテクチャに依存しないタイプで、ビット単位のデータサイズは、コードが実行されているマシンに関係なく変わりません。

今日のほとんどのシステムアーキテクチャは32ビットまたは64ビットです。例えば、最新のWindowsラップトップ向けに開発しているかもしれませんが、そのオペレーティングシステムは64ビットアーキテクチャ上で動作しています。しかし、フィットネスウォッチのようなデバイス向けに開発している場合は、32ビットアーキテクチャで作業しているかもしれません。int32のようなアーキテクチャに依存しないタイプを使用すると、コンパイル対象のアーキテクチャに関係なく、そのタイプは一定のサイズを持ちます。

2番目のタイプは実装固有のタイプです。このタイプでは、プログラムが構築されるアーキテクチャに基づいてビットサイズが変わる可能性があります。例えば、intタイプを使用する場合、Goが32ビットアーキテクチャ向けにコンパイルすると、データ型のサイズは32ビットになります。プログラムが64ビットアーキテクチャ向けにコンパイルされる場合、変数は64ビットのサイズになります。

データ型のサイズが異なるだけでなく、整数のような型も符号付き符号なしの2つの基本タイプがあります。int8は符号付き整数で、-128から127までの値を持つことができます。uint8は符号なし整数で、0から255までの正の値のみを持つことができます。

範囲はビットサイズに基づいています。バイナリデータの場合、8ビットは合計256の異なる値を表すことができます。int型は正の値と負の値の両方をサポートする必要があるため、8ビット整数(int8)の範囲は-128から127までとなり、合計256のユニークな可能な値があります。

Goには以下のアーキテクチャに依存しない整数型があります:

uint8       unsigned  8-bit integers (0 to 255)
uint16      unsigned 16-bit integers (0 to 65535)
uint32      unsigned 32-bit integers (0 to 4294967295)
uint64      unsigned 64-bit integers (0 to 18446744073709551615)
int8        signed  8-bit integers (-128 to 127)
int16       signed 16-bit integers (-32768 to 32767)
int32       signed 32-bit integers (-2147483648 to 2147483647)
int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

浮動小数点数と複素数もまた、様々なサイズで提供されています:

float32     IEEE-754 32-bit floating-point numbers
float64     IEEE-754 64-bit floating-point numbers
complex64   complex numbers with float32 real and imaginary parts
complex128  complex numbers with float64 real and imaginary parts

また、特定のデータ型に有用な名前を割り当てるエイリアス数値型もいくつかあります:

byte        alias for uint8
rune        alias for int32

byteエイリアスの目的は、プログラムがバイトデータ測定とは無関係の小さな整数ではなく、文字列要素として一般的なコンピューティング測定値としてバイトを使用していることを明確にすることです。byteuint8はプログラムがコンパイルされると同一になりますが、byteは数値形式で文字データを表すためによく使用され、uint8はプログラム内の数値として意図されています。

runeエイリアスは少し異なります。byteuint8はまったく同じデータですが、runeは1バイトまたは4バイトになることができ、int32によって決定される範囲です。runeはUnicode文字を表すために使用され、ASCII文字のみがint32データ型だけで表すことができます。

さらに、Goには以下の実装固有の型があります:

uint     unsigned, either 32 or 64 bits
int      signed, either 32 or 64 bits
uintptr  unsigned integer large enough to store the uninterpreted bits of a pointer value 

実装固有の型は、プログラムがコンパイルされるアーキテクチャによってサイズが定義されます。

数値データ型の選択

適切なサイズを選ぶ際には、通常、対象とするアーキテクチャのパフォーマンスが、扱うデータのサイズよりも重要です。しかし、プログラムのパフォーマンスに関する具体的な影響を知らなくても、最初の段階では以下のような基本的なガイドラインに従うことができます。

この記事で前述したように、アーキテクチャに依存しない型と、実装に固有の型があります。整数データについては、Goではint64uint64ではなく、intuintのような実装型を使用するのが一般的です。これにより、通常、対象とするアーキテクチャで最速の処理速度が得られます。例えば、int64を使用して32ビットアーキテクチャにコンパイルすると、データをアーキテクチャ間で移動するための追加のCPUサイクルが必要になるため、少なくとも2倍の時間がかかります。代わりにintを使用すれば、プログラムは32ビットアーキテクチャで32ビットのサイズとして定義され、処理が大幅に速くなります。

特定のサイズ範囲を超えないことがわかっている場合、アーキテクチャに依存しない型を選ぶことで、速度を向上させ、メモリ使用量を削減できます。例えば、データが100を超えず、正の数のみであることがわかっている場合、uint8を選ぶことでプログラムの効率が向上し、メモリ使用量が少なくなります。

数値データ型の可能な範囲をいくつか見てきたので、プログラムでそれらの範囲を超えた場合に何が起こるかを見てみましょう。

オーバーフロー vs. ラップアラウンド

Goは、値がコンパイル時に計算されるか実行時に計算されるかに応じて、データ型が格納するように設計された値よりも大きな値を格納しようとすると、数値をオーバーフローさせる可能性とラップアラウンドさせる可能性の両方があります。コンパイル時エラーは、プログラムがビルドしようとしたときにエラーを見つけた場合に発生します。実行時エラーは、プログラムがコンパイルされた後、実際に実行されている間に発生します。

次の例では、maxUint32をその最大値に設定します:

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 // Max uint32 size
	fmt.Println(maxUint32)
}

これはコンパイルされて実行され、以下の結果をもたらします:

Output
4294967295

実行時に1を値に加えると、それは0にラップアラウンドします:

Output
0

一方、コンパイル時に変数に1を加えてから代入するようにプログラムを変更してみましょう:

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 + 1
	fmt.Println(maxUint32)

}

コンパイル時に、コンパイラが指定されたデータ型に保持するには値が大きすぎると判断した場合、overflowエラーをスローします。これは、計算された値が指定したデータ型には大きすぎることを意味します。

コンパイラが値がオーバーフローすると判断できるため、今度はエラーをスローします:

Output
prog.go:6:36: constant 4294967296 overflows uint32

データの範囲を理解することで、将来プログラムで潜在的なバグを回避するのに役立ちます。

数値型について学んだので、次にブール値をどのように保存するかを見てみましょう。

ブール値

ブールデータ型は、trueまたはfalseの2つの値のいずれかであり、データ型として宣言する場合はboolと定義されます。ブール値は、コンピュータサイエンスにおけるアルゴリズムに情報を提供する数学の論理分野に関連する真理値を表すために使用されます。

truefalseの値は、Goでは事前に宣言された識別子であるため、それぞれ小文字のtfで表記されます。

数学の多くの演算は、真または偽のいずれかに評価される答えを与えます:

  • 大なり
    • 500 > 100 true
    • 1 > 5 false
  • 小なり
    • 200 < 400 true
    • 4 < 2 false
  • 等しい
    • 5 = 5 true
    • 500 = 400 false

数値と同様に、ブール値を変数に格納することができます:

myBool := 5 > 8

そして、fmt.Println()関数を呼び出すことでブール値を出力できます:

fmt.Println(myBool)

58より大きくないため、次のような出力が得られます:

Output
false

Goでより多くのプログラムを書くにつれて、ブール値の仕組みや、trueまたはfalseと評価されるさまざまな関数や操作がプログラムの流れをどのように変えるかについて、より理解が深まるでしょう。

文字列

文字列は、一つ以上の文字(文字、数字、記号)の並びで、定数または変数となり得ます。文字列はGoではバッククォート`または二重引用符"のいずれかで囲まれ、使用する引用符によって特性が異なります。

バッククォートを使用すると、生の文字列リテラルを作成します。二重引用符を使用すると、解釈された文字列リテラルを作成します。

生の文字列リテラル

生の文字列リテラルは、バッククォート(バックティックとも呼ばれる)で囲まれた文字の並びです。引用符内のどの文字も、バッククォート文字自体を除いて、バッククォート間に表示される通りに表示されます。

a := `Say "hello" to Go!`
fmt.Println(a)
Output
Say "hello" to Go!

通常、バックスラッシュは文字列内の特殊文字を表すために使用されます。例えば、解釈される文字列内で、\nは文字列内の新しい行を表します。しかし、バックスラッシュは生の文字列リテラル内では特別な意味を持ちません:

a := `Say "hello" to Go!\n`
fmt.Println(a)

文字列リテラル内でバックスラッシュに特別な意味がないため、実際には\nの値を出力し、新しい行を作成しません:

Output
Say "hello" to Go!\n

生の文字列リテラルは、複数行の文字列を作成するためにも使用できます:

a := `This string is on 
multiple lines
within a single back 
quote on either side.`
fmt.Println(a)
Output
This string is on multiple lines within a single back quote on either side.

前述のコードブロックでは、新しい行が入力から出力へそのまま引き継がれました。

解釈される文字列リテラル

解釈される文字列リテラルは、"bar"のようにダブルクォーテーションで囲まれた文字列です。クォーテーション内では、改行とエスケープされていないダブルクォーテーションを除くあらゆる文字が現れることがあります。解釈される文字列内でダブルクォーテーションを表示するには、次のようにバックスラッシュをエスケープ文字として使用できます:

a := "Say \"hello\" to Go!"
fmt.Println(a)
Output
Say "hello" to Go!

ほとんどの場合、解釈される文字列リテラルを使用します。なぜなら、それらはエスケープ文字を許可しているからです。文字列の扱い方について詳しくは、Goでの文字列操作の紹介をチェックしてください。

UTF-8文字を含む文字列

UTF-8は、文字を1バイトから4バイトまでの可変幅でエンコードするためのエンコーディング方式です。Goは特別なセットアップ、ライブラリ、パッケージなしでUTF-8文字をサポートしています。文字Aのようなローマ字は、数字65のようなASCII値で表現できます。しかし、のような国際的な特殊文字では、UTF-8が必要になります。GoはUTF-8データにruneというエイリアス型を使用します。

a := "Hello, 世界"

Goでは、forループ内でrangeキーワードを使用して、UTF-8文字列を含む任意の文字列をインデックス化することができます。forループとrangeについては、後のシリーズでより深く説明しますが、現時点では、これを使用して特定の文字列のバイト数をカウントできることを知っておくことが重要です。

package main

import "fmt"

func main() {
	a := "Hello, 世界"
	for i, c := range a {
		fmt.Printf("%d: %s\n", i, string(c))
	}
	fmt.Println("length of 'Hello, 世界': ", len(a))
}

上記のコードブロックでは、変数aを宣言し、それにHello, 世界という値を代入しました。代入されたテキストにはUTF-8文字が含まれています。

その後、標準のforループとrangeキーワードを使用しました。Goでは、rangeキーワードは文字列をインデックス化し、一度に1文字ずつ、そしてその文字が文字列内のどのバイトインデックスにあるかを返します。

fmt.Printf関数を使用して、フォーマット文字列%d: %s\nを提供しました。%dは数字(この場合は整数)のプリント動詞であり、%sは文字列のプリント動詞です。次に、forループの現在のインデックスであるiと、forループの現在の文字であるcの値を提供しました。

最後に、組み込みのlen関数を使用して、変数aの全体の長さを出力しました。

以前に、ルーンはint32のエイリアスであり、1から4バイトで構成されることを述べました。という文字は3バイトで定義され、UTF-8文字列を範囲指定する際にインデックスがそれに応じて移動します。これが、iが出力されるときに連続していない理由です。

Output
0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界': 13

ご覧のように、長さは文字列を範囲指定するのにかかった回数よりも長くなっています。

常にUTF-8文字列を扱うわけではありませんが、UTF-8文字列を扱う場合は、それらがルーンであり、単一のint32ではない理由を理解できるようになりました。

変数のデータ型を宣言する

異なるプリミティブデータ型について知ったので、Goでこれらの型を変数に割り当てる方法を説明します。

Goでは、キーワードvarに続けて変数の名前と希望するデータ型を指定することで、変数を定義できます。

以下の例では、float64型のpiという変数を宣言します。

キーワードvarが最初に宣言されます:

var pi float64

次に、変数の名前piが続きます:

var pi float64

そして最後にデータ型float64が宣言されます:

var pi float64

オプションで、初期値を指定することもできます。例えば3.14

var pi float64 = 3.14

Goは静的型付け言語です。静的型付けとは、プログラム内の各ステートメントがコンパイル時にチェックされることを意味します。また、データ型が変数に関連付けられることを意味しますが、動的型付け言語では、データ型は値に関連付けられます。

例えば、Goでは、変数を宣言する際に型が宣言されます:

var pi float64 = 3.14
var week int = 7

これらの変数は、宣言方法によって異なるデータ型になる可能性があります。

これは、データ型が値に関連付けられるPHPのような言語とは異なります:

$s = "sammy";         // $sは自動的に文字列になります
$s = 123;             // $sは自動的に整数になります

前述のコードブロックでは、最初の$sは値"sammy"が割り当てられているため文字列であり、2番目は値123を持つため整数です。

次に、配列のようなより複雑なデータ型を見てみましょう。

配列

配列は、要素の順序付きシーケンスです。配列の容量は作成時に定義されます。一度配列がそのサイズを割り当てると、サイズを変更することはできません。配列のサイズは静的であるため、メモリを一度だけ割り当てることを意味します。これにより、配列は扱いにくいものになりますが、プログラムのパフォーマンスが向上します。このため、配列は通常、プログラムの最適化時に使用されます。スライスは、次に説明するように、より柔軟性があり、他の言語での配列と考えられるものです。

配列は、配列のサイズを宣言し、次にデータ型を宣言し、値を波括弧{ }で定義することで定義されます。

文字列の配列は次のようになります:

[3]string{"blue coral", "staghorn coral", "pillar coral"}

配列を変数に格納して出力することができます:

coral := [3]string{"blue coral", "staghorn coral", "pillar coral"}
fmt.Println(coral)
Output
[blue coral staghorn coral pillar coral]

前述のように、スライスは配列に似ていますが、はるかに柔軟です。この可変データ型を見てみましょう。

スライス

スライスは、長さを変更できる要素の順序付きシーケンスです。スライスはサイズを動的に増やすことができます。新しいアイテムをスライスに追加するとき、スライスに新しいアイテムを格納するのに十分なメモリがない場合、必要に応じてシステムからより多くのメモリを要求します。スライスは、必要に応じて要素を追加するために拡張できるため、配列よりも一般的に使用されます。

スライスは、データ型の前に開きと閉じの角括弧 `[]` を付け、波括弧 `{ }` の間に値を持つことで定義されます。

整数のスライスは次のようになります:

[]int{-3, -2, -1, 0, 1, 2, 3}

浮動小数点数のスライスは次のようになります:

[]float64{3.14, 9.23, 111.11, 312.12, 1.05}

文字列のスライスは次のようになります:

[]string{"shark", "cuttlefish", "squid", "mantis shrimp"}

文字列のスライスを `seaCreatures` として定義しましょう:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"}

変数を呼び出すことでそれらを出力できます:

fmt.Println(seaCreatures)

出力は、作成したリストとまったく同じように見えます:

Output
[shark cuttlefish squid mantis shrimp]

スライスにアイテムを追加するために `append` キーワードを使用できます。次のコマンドは、スライスに文字列値 `seahorse` を追加します:

seaCreatures = append(seaCreatures, "seahorse")

出力して確認できます:

fmt.Println(seaCreatures)
Output
[shark cuttlefish squid mantis shrimp seahorse]

ご覧のように、未知のサイズの要素を管理する必要がある場合、スライスは配列よりもはるかに柔軟です。

マップ

`map` は Go の組み込みのハッシュまたは辞書型です。マップは `キー` と `` をペアで使用してデータを格納します。これは、インデックス(この場合はキー)によって値を迅速に検索するプログラミングにおいて便利です。たとえば、ユーザー ID でインデックス付けされたユーザーのマップを保持したい場合があります。キーはユーザー ID になり、ユーザーオブジェクトが値になります。マップは、キーワード `map` を使用し、その後にキーのデータ型を角括弧 `[ ]` で囲み、値のデータ型を続け、波括弧内にキーと値のペアを配置することで構築されます。

map[key]value{}

通常、関連性のあるデータを保持するために使用され、例えばIDに含まれる情報のようなものです。マップは以下のようになります:

map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}

波括弧に加えて、マップ内にはコロンが散在していることに気づくでしょう。コロンの左側の単語はキーです。キーはGo言語内の任意の比較可能な型であり得ます。比較可能な型は、stringsintsなどのプリミティブ型です。プリミティブ型は言語によって定義され、他の型を組み合わせることで構築されるものではありません。ユーザー定義型であることも可能ですが、プログラミングエラーを避けるためにはシンプルに保つことがベストプラクティスとされています。上記の辞書のキーは、nameanimalcolor、およびlocationです。

コロンの右側の単語は値です。値は任意のデータ型で構成され得ます。上記の辞書の値は、Sammysharkblue、およびoceanです。

マップを変数内に格納し、それを出力してみましょう:

sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
fmt.Println(sammy)
Output
map[animal:shark color:blue location:ocean name:Sammy]

もしSammyの色を分離したい場合、sammy["color"]を呼び出すことでそれが可能です。それを出力してみましょう:

fmt.Println(sammy["color"])
Output
blue

マップはデータをキーと値のペアで保存するため、Goプログラムにおいて重要な要素となり得ます。

結論

この時点で、Goで使用可能な主要なデータ型のいくつかをより良く理解しているはずです。これらの各データ型は、Go言語でプログラミングプロジェクトを開発する際に重要になります。

Goで利用可能なデータ型をしっかりと理解したら、状況に応じてデータ型を変更するためにデータ型の変換方法を学ぶことができます。

Source:
https://www.digitalocean.com/community/tutorials/understanding-data-types-in-go