【GO言語】TypeScriptと違う!!(⚠️ GO / 配列 - Arrays編)
はじめに
筆者はTypeScriptを用いてWebフロントエンド開発を3年間行ってきました。
ただ最近になって、静的型付け言語で且つバックエンド開発に重点を置きたいと思い、GO言語を学んでいます!
この記事では、TypeScript(JavaScript)で学んできて、GO言語に入門したときにつまづいた部分についての記録を残すために、自分用の記事として位置付けをしています!
GOにおける配列の定義
配列型の定義では、長さと要素の型を指定する。
例えば、[4]int型は4つの整数の配列を表します。配列のサイズは固定であり、長さは型の一部である([4]intと[5]intは互換性のない別個の型である)。
配列は通常の方法でインデックスを付けられるので、s[n]という式は0から始まるn番目の要素にアクセスすることとなる。
package main
import "fmt"
func main() {
var a [4]int
a[0] = 1
i := a[0]
fmt.Printf("i:%v\n", i) // i == 1
}
固定長なので、定義した長さ以上のインデックスに配列を代入することはできない。
package main
func main() {
var a [4]int
a[5] = 1
// # command-line-arguments
//./main.go:5:4: invalid argument: index 5 out of bounds [0:4]
}
TypeScript(JavaScript)の配列では、
固定長ではなく可変長として扱われるので、ここがGO言語の配列と大きな違いである👀
(GOには配列とは別にスライスが用意されており、TypeScript(JavaScript)の配列に似た性質は、スライスが挙げられる。)
配列の定義をざっくり共有したところで、
以下ではTypeScript(JavaScript)とGOの配列の違いを2点お伝えします。
TypeScript(JavaScript)とGOの配列の違い
1点目 - 配列の初期化時の初期値
TypeScript(JavaScript)
TypeScript(JavaScript)では、
配列を変数宣言しただけでは、値となるデータを配列に含めることができません。
const array:number[] = [];
console.log(array[0]); // undefined
GO
Go言語では、
変数宣言をしたタイミングで型の初期値が含まれるようになります。
(例の場合は、Int型なので初期値は0)
package main
import "fmt"
func main() {
var a [1]int
fmt.Printf("Value: %v, Capacity: %v, Length: %v\n", a, cap(a), len(a)) // Value: [0], Capacity: 1, Length: 1
}
⇩⇩型の初期値についての参考記事⇩⇩
なので、
GO言語の場合は以下のように変数を宣言行い、配列型のデータを見ると0が3つ定義されます。
package main
import "fmt"
func main() {
var a [3]int
var b [3]string
var c [3]bool
var d [3]error
fmt.Printf("int:%v, string:%v, bool:%v, error:%v\n", a, b, c, d)
// int:[0 0 0], string:[ ], bool:[false false false], error:[<nil> <nil> <nil>]
}
GOで配列リテラルを定義したい場合はどうなるのか
package main
import "fmt"
func main() {
a := [3]int{1, 10, 100}
fmt.Printf("Value: %v, Capacity: %v, Length: %v\n", a, cap(a), len(a)) // Value: [1 10 100], Capacity: 3, Length: 3
}
package main
import "fmt"
func main() {
+ a := [...]int{1, 10, 100}
- a := [3]int{1, 10, 100}
fmt.Printf("Value: %v, Capacity: %v, Length: %v\n", a, cap(a), len(a)) // Value: [1 10 100], Capacity: 3, Length: 3
}
2点目 - 配列の参照
TypeScript(JavaScript)
TypeScript(JavaScript)では、
オブジェクトや配列を上記のように異なる変数に代入した際は、代入先の変数「b」が参照する配列は、代入元の変数「a」が参照する先の配列と同じになります。
よって、変数「b」の配列の中身を操作した場合は、変数「a」の配列の中身を操作したと同義になり、出力結果は同値となります。
const a:number[] = [0];
const b = a; // 配列が参照しているメモリアドレスを共有
b[0] = 1;
console.log({a, b}, a === b); // { a: [ 1 ], b: [ 1 ] } true
GO
Go言語では、
配列は値として扱われます。
よって、変数「a」から「b」への代入ではその内容が値コピーされるので、代入先の変数「b」の中身を変更しても代入元の変数「a」の値は変化しません。
以下より、変数「a」と「b」のポインタ変数の値が異なることわかります。
package main
import "fmt"
func main() {
var a [1]int
b := a // 値渡し(コピー)
b[0] = 1
fmt.Printf("a:%v, b:%v, a == b:%v\n", a, b, a == b)
// a:[0], b:[1], a == b:false
fmt.Printf("pointer a:%p, pointer b:%p, pointer a[0]:%p, pointer b[0]:%p\n", &a, &b, &a[0], &b[0])
// pointer a:0xc0000120f8, pointer b:0xc000012100, pointer a[0]:0xc0000120f8, pointer b[0]:0xc000012100
}
ただGO言語では、
「&値」とすることでポインタを渡すことができます。
上記の例では、変数「a」から「b」にポインタを共有することになるので、コピー先の変数「b」の中身を変更した場合はコピー元の変数「a」の値も変化します。
以下より、変数「a」と「b」のポインタ変数の値が同じことがわかります。
package main
import "fmt"
func main() {
var a [1]int
+ b := &a
- b := a
b[0] = 1
+ fmt.Printf("a:%v, b:%v, a == b:%v\n", a, b, a == *b)
- fmt.Printf("a:%v, b:%v, a == b:%v\n", a, b, a == b)
// a:[1], b:&[1], a == b:true
+ fmt.Printf("pointer a:%p, pointer b:%p, pointer a[0]:%p, pointer b[0]:%p\n", &a, b, &a[0], &(*b)[0])
()
// pointer a:0xc000110018, pointer b:0xc000110018, pointer a[0]:0xc000110018, pointer b[0]:0xc000110018
}
この挙動に大変深く関わりのあるポインタとアドレスについての説明は、当記事のスコープが外なので割愛させていただきます。
参考記事だけ置いておきます。
⇩⇩ポインタとアドレスについての参考記事⇩⇩
ネストした配列の場合はどうなるのか
TypeScript(JavaScript)
const a:(number[])[] = [[0]];
const b = a; // 配列が参照しているメモリアドレスを共有
b[0][0] = 1;
console.log({a, b}, a === b, a[0] === b[0]); // { a: [ [ 1 ] ], b: [ [ 1 ] ] } true true
GO
package main
import "fmt"
func main() {
var a [1][1]int
b := a // 値渡し(コピー)
b[0][0] = 1
fmt.Printf("a:%v, b:%v, a == b:%v, a[0] == b[0]:%v\n", a, b, a == b, a[0] == b[0])
// a:[[0]], b:[[1]], a == b:false, a[0] == b[0]:false
fmt.Printf("pointer a:%p, pointer b:%p, pointer a[0]:%p, pointer b[0]:%p\n", &a, &b, &a[0], &b[0])
// pointer a:0xc00009e018, pointer b:0xc00009e020, pointer a[0]:0xc00009e018, pointer b[0]:0xc00009e020
}
package main
import "fmt"
func main() {
var a [1][1]int
+ b := &a // 配列が参照しているポインタの共有
- b := a // 値渡し(コピー)
b[0][0] = 1
+ fmt.Printf("a:%v, b:%v, a == b:%v, a[0] == b[0]:%v\n", a, b, a == *b, a[0] == (*b)[0])
- fmt.Printf("a:%v, b:%v, a == b:%v, a[0] == b[0]:%v\n", a, b, a == b, a[0] == b[0])
// a:[[1]], b:&[[1]], a == b:true, a[0] == b[0]:true
+ fmt.Printf("pointer a:%p, pointer b:%p, pointer a[0]:%p, pointer b[0]:%p\n", &a, b, &a[0], &(*b)[0])
- fmt.Printf("pointer a:%p, pointer b:%p, pointer a[0]:%p, pointer b[0]:%p\n", &a, &b, &a[0], &b[0])
// pointer a:0xc0000a0018, pointer b:0xc0000a0018, pointer a[0]:0xc0000a0018, pointer b[0]:0xc0000a0018
}
配列の中身のポインタも渡すことが可能である。
package main
import "fmt"
func main() {
var a [3][2]int
b := &a[0] // 配列が参照しているポインタの共有
b[0] = 1
b[1] = 5
fmt.Printf("a:%v, b:%v, a[0] == b:%v\n", a, *b, a[0] == *b)
// a:[[1 5] [0 0] [0 0]], b:[1 5], a[0] == b:true
fmt.Printf("pointer a[0]:%p, pointer b:%p, pointer a[0][0]:%p, pointer b[0]:%p, pointer a[0][1]:%p, pointer b[1]:%p\n", &a[0], b, &a[0][0], &(*b)[0], &a[0][1], &(*b)[1])
// pointer a[0]:0xc000016180, pointer b:0xc000016180, pointer a[0][0]:0xc000016180, pointer b[0]:0xc000016180, pointer a[0][1]:0xc000016188, pointer b[1]:0xc000016188
}
結果は前項説明通り、同様である。
まとめ
いかがだったでしょうか??
配列1つとっても言語によって性質が大きく異なることを学びました。
TypeScript(JavaScript)では、
オブジェクトや配列の変数への代入はメモリアドレスの共有と言われ、メモリアドレスを共有していることが注意点だったのですが、GOでは値渡しだということを確認することができました。
また、値が示すポインタを、異なる変数へ代入するとメモリアドレスを共有できることを学びました。
言語によってこういった違いがあることに注意して実装していかないと、潜在的なバグに繋がる可能性が広がるので、公式ドキュメント見つつ動作確認することの大切さ、そして何よりアウトプットして理解を深める大切さを再認識できました。
引き続き、GOを学びつつTypeScript(JavaScript)との違いを認識して、Zennを通じてアウトプットしていきたいと思います。
参考記事
Discussion