🐻‍❄️

TypeScriptユーザーから見るGoの特徴

2024/10/10に公開

はじめに

この記事では、TypeScriptとGoの違い(基本的にGoにはあってTypeScriptにはないこと)をまとめています。
これからGoを学ぼうと考えている方や、TypeScriptユーザーがGoに触れてみる際の参考になれば幸いです!

また私自身はまだまだ初学者なので、もしかしたら間違いや改善点があるかもしれません。
その場合はぜひアドバイスをいただけると嬉しいです🙇

変数

暗黙的宣言

varを省略してより簡潔に変数を宣言することができます。
この際、変数の型は自動的に推論されます。
ただし、暗黙的な宣言は関数内でのみ行うことができます。

// 暗黙的な変数の宣言(型指定は不要)
// 宣言した時点で変数の型が決まります
// 変数名 := 値
// ここでは、zはint型として宣言されます
z := 100

コードをシンプルにすることができます。

まとめて宣言

複数の変数をまとめて宣言することができます。
これにより宣言を簡潔にし、コードの可読性を向上させることができます。

var (
    apple int = 100
    banana string = "バナナ"
)

まとめて宣言することで、関連する変数を一箇所で管理でき、コードがすっきりとします。

初期値を指定せずに宣言した場合、各型の初期値が設定される

変数を初期値なしで宣言すると、各型に応じた初期値が自動的に設定されます。
これにより、プログラムの安定性が向上します。

// mojiは空文字列で初期化されます
var moji string

// suujiは整数の0で初期化されます
var suuji int

// singiはブール値のfalseで初期化されます
var singi bool

初期値を明示的に指定しなくても、これらのゼロ値が自動的に割り当てられるため、TypeScriptのundefinedのような未定義の状態にはなりません。

関数

関数の戻り値は処理結果とエラーを返す

関数の戻り値として処理結果に加えてエラーを返すことができます。
これにより、関数の呼び出し元はエラーが発生したかどうかを簡単に確認でき、適切なエラーハンドリングが可能になります。

// strconvは文字列から数値に変換する関数
// _は変数を使用しない場合に使用する
// 2つ目の戻り値はエラーを返す
i3 , err :=  strconv.Atoi(s2)
if err != nil {
  // エラーハンドリング
  fmt.Println(err)

戻り値に変換結果とエラーが返され、エラーが発生した場合はerrにエラー情報が格納されます。
if err != nilの条件文を使って、エラーが発生したかどうかをチェックし、エラーメッセージを表示することができます。

配列とスライス

Go言語の配列は固定サイズであり、TypeScriptの配列より制限が厳しくなっています。
一方、スライスは可変長であり、より柔軟なデータ構造です(TypeScriptの配列に似ている)。

// 配列の宣言(サイズは固定)
arr := [4]int{1, 2, 3, 4}

// 要素のアクセス
fmt.Println(arr[0]) // 1

// 配列の長さは固定
fmt.Println(len(arr)) // 4

// 要素を追加しようとするとエラーになる(配列のサイズは固定なので)
arr = append(arr, 5) 

配列のサイズは固定されており、要素数を超えて追加することはできません。
そのため、append関数を使おうとするとコンパイルエラーが発生します。

// スライスの宣言と初期化
slice := []int{1, 2, 3, 4}

// 要素の追加
slice = append(numbers, 5) // [1, 2, 3, 4, 5]

// 要素のアクセス
fmt.Println(slice[0]) // 1

// スライスの長さと容量
fmt.Println(len(slice)) // 5
fmt.Println(cap(slice)) // 容量の値は内部の配列次第

スライスは、可変長のデータ構造であり、初期化後に要素を追加することができます。
このように、Goの配列とスライスは似ている型ですが、それぞれ異なる特性を持っているので、用途に応じて使い分けることが重要です。

機能 TypeScriptの配列 Goのスライス Goの配列
サイズ 可変長 可変長 固定長
要素の追加 push() などで可能 append() で可能 追加は不可
要素のアクセス arr[0] でアクセス slice[0] でアクセス arr[0] でアクセス
長さの取得 arr.length len(slice) len(arr)
容量の取得 不要(自動管理) cap(slice) 固定のため不要

メソッド

Goでは型(構造体)に対して関数を紐づけることを「メソッド」と呼びます。
メソッドを使用することで、特定の型に関連する処理を一つのまとまりとして表現できます。

// 構造体の定義
type Person struct {
    Name string
    Age  int
}

// メソッドの定義
func (p Person) greet() string {
    return "Hello, my name is " + p.name
}

func main() {
    person := Person{name: "John", age: 30}
    fmt.Println(person.greet())  // 出力: Hello, my name is John
}

Personという構造体を定義し、その中にnameageというフィールドを持たせています。
greetというメソッドは、Person型のインスタンスに対して呼び出され、そのnameフィールドを使用して挨拶文を生成します。
main関数内でpersonというPerson型のインスタンスを作成し、greetメソッドを呼び出すことで、"Hello, my name is John"というメッセージが出力されます。

interface

GoのinterfaceはTypeScriptのinterfaceとは大きく性質が異なります。
Goにおけるinterfaceはメソッドの集合を定義し、異なる型に共通の振る舞いを持たせるための手段です。
interfaceを実装することで、異なる型のオブジェクトが同じメソッドを持つことが保証されます。

type Animal interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}

func (c Cat) Speak() string {
    return "Meow!"
}

// animalの型をDogやCatにしなくてOK
func printAnimalSound(animal Animal) {
    fmt.Println(animal.Speak())
}

func main() {
    dog := Dog{}
    cat := Cat{}
    
    printAnimalSound(dog) // 出力: Woof!
    printAnimalSound(cat) // 出力: Meow!
}

この例では、Animalというinterfaceを定義し、Speakメソッドを要求しています。
DogCatの構造体はそれぞれこのメソッドを実装しており、printAnimalSound関数を通じて、interface型の引数として渡すことができます。
これにより、異なる動物の鳴き声を一つの関数で出力できるようになります。
型の柔軟性を高め、コードの再利用性を促進する強力な機能です。

その他

ポインタ

ポインタは、変数のアドレスを指し示すための変数です。
ポインタを使用することで、関数内で変更した値を変数へ反映させたり、メモリの効率的な管理を行ったりすることができます。

type Person struct {
    name string
    age  int
}

// 
func updateAge(p Person) {
    p.age = 30
}

func main() {
    person := Person{name: "John", age: 25}
    updateAge(person)
    fmt.Println(person.age)  // 出力: 25 (コピーされたため、元のデータは変わらない)
}

updateAge関数はPerson型の値を引数として受け取ります。
Goの基本データ型は値渡しであるため、関数内での変更は元のデータに反映しません。
したがって、出力は元の年齢のままです。

次に、ポインタを使用して同じ処理を行う例です。

// *をつけることでポイント型を指定できる
func updateAge(p *Person) {
    p.age = 30
}

func main() {
    person := Person{name: "John", age: 25}
    updateAge(&person)  // ポインタを渡す
    fmt.Println(person.age)  // 出力: 30 (元のデータが変更される)
}

updateAge関数は*Person型のポインタを引数に取ります。&personを渡すことで、personのアドレスを渡し、関数内でそのアドレスを通じて元のデータを直接変更できます。
そのため、出力は変更された年齢になります。

ポインタの主な利点は、値型の変数を渡すときに発生するコピーのオーバーヘッドを削減できることです。
特に大きなデータ構造やオブジェクトを扱う場合、ポインタを使うことで効率的にメモリを管理し、性能を向上させることができます。

TypeScriptでも値型(プリミティブ型)と参照型(オブジェクトや配列)の概念は存在しますが、Goではポインタという仕組みがあるため、参照を明示的に扱う必要があります。
ポインタを使用してアドレスを操作することができるため、メモリ管理やパフォーマンスの面でより意識する必要があります。
この点で、GoはTypeScriptよりも低レベルなメモリ操作が可能です。

ゴルーチンとチャネル

ch := make(chan int) // チャネルの作成

go func() {          // ゴルーチンの開始
    ch <- 42        // チャネルにデータを送信
}()

result := <-ch      // チャネルからデータを受信
fmt.Println(result) // 42が出力される

ゴルーチンは、軽量なスレッドのようなもので、関数を並行して実行するための仕組みです。
Goでは、goキーワードを使って関数を非同期に実行することができ、チャネルを利用してゴルーチン間でデータをやり取りします。

チャネルは、ゴルーチン同士が安全にデータを送受信するための仕組みで、上記の例では、整数値 42 をゴルーチンからメインゴルーチンに送信し、それを出力しています。

Goの並行処理は、複数のスレッドを使って同時に実行されるため、並行性が高くマルチスレッドの要素を持っています。
一方、TypeScriptでは非同期処理(async/awaitPromise)が用いられますが、これはシングルスレッド上で実行され、同期処理を終えた後にイベントループによって非同期処理が順次実行されます。

このように、Goのゴルーチンは並行処理をマルチスレッドで実現し、TypeScriptの非同期処理はイベントループで処理を順次行う点で大きな違いがあります。

終わりに

この記事では、普段TypeScriptを使っている私が学びながら感じた、TypeScriptにはないGoの特徴を紹介しました。
個人的にはゴルーチンとチャネルを使った並行処理の理解にかなり苦戦しました💦
自分でスレッドを増やしたり、キューの管理を行う感覚はTypesciptではなかなか味わえないと思います。(改めてプログラミングの奥深さを感じました。。)

引き続き学びを深めていきたいと思います!💫

Discussion