TypeScriptエンジニアが学ぶGo言語
TypeScriptとGo言語のざっくり比較
| 項目 | TypeScript | Go |
| --------- | --------------------------------- | -------------------------------------------------------- |
| 実行環境 | Node.jsなど | ネイティブバイナリ生成 |
| 型 | 静的だが柔軟<br>(型推論・Union型など) | 静的でシンプル<br>(型推論少なめ) |
| クラス | あり<br>OOP(オブジェクト指向プログラミング)中心 | なし<br>(構造体 + interface) |
| 非同期処理 | Promise, async/await | goroutine, channel |
| パッケージ管理 | npm, yarn | go mod |
| null安全 | null, undefined, ?記法 | nilのみ、nilチェックが必要 |
| エラーハンドリング | try-catch | error型+明示的処理 |
| 配列・マップ | Array, Map | slice, map |
| ジェネリクス | v4以降のTypeScriptで豊富<br>(arg: T): T | Go1.18から導入、まだ制限あり<br>`func Sum[T Number](arg T) T {...}` |
Hello, World!
TypeScript
function greet(name: string): string {
return Hello, ${name}!;
}Go
import "fmt"
func greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}ポイント
- fmt.Sprintfは文字列フォーマット関数。
classに相当する、構造体とインターフェース
TODO: TypescriptでClassを使っていない。クラスに相当するものをGoで使いたい時に面食らう
TypeScript
```ts
interface Person {
name: string;
greet(): string;
}
const p: Person = {
name: "みけ",
greet() {
return こんにちは、${this.name}です。;
}
}
const main = () => {
console.log(p.greet()); // こんにちは、みけです。
}
```
Go
```go
type Person struct {
Name string
}
func (p Person) Greet() string {
return fmt.Sprintf("こんにちは、%sです。", p.Name)
}
func main() {
p := Person{Name: "みけ"}
fmt.Println(c.Greet()) // こんにちは、みけです。
}
```
ポイント
- structはオブジェクトっぽいやつ
- メソッド定義は
(p Person)←レシーバ(thisに近い) - Goの関数名の頭文字が大文字。Goでは小文字はプライベート扱い
非同期処理・並行処理
TypeScript
```ts
async function doSomething() {
}
async function run() {
await doSomething();
console.log("done");
}
```
Go
```go
import (
"fmt"
"time"
"sync"
)
func do1() {
time.Sleep(1 * time.Second)
fmt.Println("2")
}
func do2(wg *sync.WaitGroup) { // アスタリスクはポインタ型で、ポインタ渡しされることを示す
defer wg.Done() // 関数が終了したらWaitGroupを減らす
time.Sleep(3 * time.Second)
fmt.Println("4")
}
func main() {
var wg sync.WaitGroup
fmt.Println("1")
do1()
wg.Add(1)
go do2(&wg) // &を付けてポインタ渡しをする
fmt.Println("3")
wg.Wait() // 全てのゴールーチンが終わるまで待つ
}
// 1 2 3 4 で出力される
```
ポイント
- Go言語では基本的に同期処理される。goキーワードを関数呼び出しの前に付けることで、その関数はゴールーチンとして並列に実行される。
- ポインタ渡し(参照渡し)はGo言語では推奨されることが多い。Go言語では基本的にオブジェクトも値渡しされる(大きなオブジェクトでは暗黙的に参照渡しされる)
- TODO: どれくらい大きなオブジェクトだと参照渡しになる?
参照渡し
TypeScript
```ts
const cat = {
name: "シロ"
}
const changeName = (cat) => {
cat.name = "ポチ"
}
const main = () => {
changeName(cat);
console.log(cat.name); // ポチ
}
```
Go
```go
import (
"fmt"
)
type Cat struct {
Name string
}
func changeNameByValue(cat Cat) {
cat.Name = "ポチ"
}
func changeNameByPointer(cat *Cat) {
cat.Name = "ミケ"
}
func main() {
cat := Cat{Name: "シロ"}
changeNameByValue(cat)
fmt.Println(cat.Name) // シロ
changeNameByPointer(&cat)
fmt.Println(cat.Name) // ミケ
}
```
ポイント
- アンパサンドでポインタの明示をしている。Go言語ならではの美しさ。
配列とスライス
TypeScript経験者が混乱しやすい部分。TSと同じ感覚で使えるのはスライス。
Goでは配列はあまり使わず、スライスが主流。
[[Go 配列とスライスの違い]]
Go
```go
import "fmt"
func changeArray(arr [3]string) {
arr[0] = "シロ"
}
func changeSlice(slice []string) {
slice[0] = "シロ"
}
func main() {
var catsArray [3]string = [3]string {"クロ", "ミケ", "ハチワレ"}
changeArray(catsArray)
fmt.Println(catsArray) // クロ ミケ ハチワレ
catsSlice := [] string{"クロ", "ミケ", "ハチワレ"}
changeSlice(catsSlice)
fmt.Println(catsSlice) // シロ ミケ ハチワレ ← 参照渡しなので変化する
}
```
ポイント
- 配列は固定長、スライスは可変長で作成。
- 配列は関数に渡した時点で、完全にコピーされる。よって、元の配列は変化しない。
- スライスの実態はポインタ、長さ、容量からなる「小さい構造体」。中身のポインタが共有されるので、変更が反映される。
interface
TypeScriptと比べると、若干クセがある。
[[Go interfaceまとめ]]
TypeScript
```ts
interface Animal {
speak(): string;
}
class Dog implements Animal {
speak() {
return "ワン!";
}
}
```
Go
```go
type Animal interface {
Speak() string
}
type Dog struct {}
type Cat struct {}
func (d Dog) Speak() string {
return "ワン!"
}
func (c Cat) Speak() string {
return "ニャー!"
}
// それぞれの動物に対して、Speak()の呼び出しを1つ書くだけで鳴かすことができる(多態性)
func main() {
animals := []Animal{Dog{}, Cat{}}
for _, a := range animals {
fmt.Println(a.Speak())
}
}
// ワン! ニャー!
```
ポイント
- Goではimplementsを書かない。明示しなくても、自動的に判定される構造的型システム。
- 上記のGoのサンプルでは、DogもCatもSpeak()を定義しているので、勝手にAnimalとして扱える。
- 多態性を再現できる。
- この書き方はGoの強み。OOPよりもインターフェースによる抽象化に重点を置いた設計になっている。
空インターフェース
```go
var x interface{}
```
これは「どんな型でも代入できる変数」である。
TypeScriptで言うと`any`に近いが、Goでは安全に使うために「型チェック」をする必要がある。
```go
import "fmt"
func printAnything(x interface{}) {
fmt.Println(x)
}
printAnything(42) // int
printAnything("Hello") // string
printAnything(true) // bool
// どれも通る
```
```go
import "fmt"
type Animal interface {
Speak() string
}
type Dog struct {}
type Cat struct {}
func (d Dog) Speak() string {
return "ワン!"
}
func (c Cat) Speak() string {
return "ニャー!"
}
func main() {
speakAny(Dog{})
speakAny(Cat{})
speakAny("これは文字列です")
speakAny(12345)
}
func speakAny(a interface{}) { // voidなので型は書かない
// 型アサーションでaがAnimalか確認
if animal, ok := a.(Animal); ok {
fmt.Println(animal.Speak()) // 型アサーション後の変数を使用
} else {
fmt.Println("これは動物ではありません")
}
}
// ワン!
// ニャー!
// これは動物ではありません
// これは動物ではありません
```