Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

A Tour of Go First Impression

バックエンドでGo Langを使う案件や会社のお付き合いが最近ぐっと増えたため、Go Langの基礎文法を学びます。A Tour of Goをざっと回したので、メモ。

変数の型推論と初期化

:=を使うことで、変数の型推論と代入が可能。

「短変数宣言(short variable declaration)」と言うらしい。また、この宣言はグローバルにはできず、関数内部が最大のスコープとなる。

 a,b,c := 10, "10", false;

簡単にdeferが使える

TypeScriptでは、async/awaitが必須のdeferがこれだけで実現できるのは驚き。意味合い的には、try-catchfinallyに近いなぁ。 deferは後入れ先出しで、最後にdeferしたものから順番に実行される。

package main

import "fmt"

func main() {
    fmt.Println("start")

    defer fmt.Println("end") // ← 最後に実行される

    fmt.Println("middle")
}

固定長の配列は、array。可変長はslice

a := [3]int{1, 2, 3}

s := make([]int, 3)

for文は、rangeを使う

// for..in的なループ
// 自動的に第1引数にindexが入る
for index, value := range items {
    fmt.Println(index, value)
}
// index使わない場合はこ
for _ , v := range nums {
    fmt.Println(i, v)
}
m = make(map[string]int)
// mapはこうなる
for key, value := range m {
    fmt.Println(key, value)
}
// Pythonと同じでStringも回せる
for i, r := range "GoLang" {
    fmt.Println(i, string(r))
}

Goの関数はクロージャ

func counter() func() int {
    x := 0
    return func() int {
        x++
        return x
    }
}

func main() {
    c := counter()

    fmt.Println(c()) // 1
    fmt.Println(c()) // 2
    fmt.Println(c()) // 3
}

cxの内容を常に記憶してるわけですな。

Struct

  • Goにはクラスがない。それは全然OK。クラスに紐づけた関数を作る場合は、レシーバーつき関数を作ることで代替するらしい。謎い。
  • 参照渡しをしないと、Structで定義した変数の中身が変わらない。値渡しをするケースがあまり想像できないな。
type User struct {
    Name string
}

func (u User) ChangeByValue() {
    u.Name = "Value"
}

func (u *User) ChangeByPointer() {
    u.Name = "Pointer"
}

func main() {
    user := User{Name: "Init"}

    user.ChangeByValue()
    fmt.Println(user.Name) // Init (変更されない)
    user.ChangeByPointer()
    fmt.Println(user.Name) // Pointer(変更される)
}

interface

  • implementsがない。シグネチャが同じなら同じTypeだとみなされるようだ。へー。
type Greeter interface {
    Greet() string
}

type User struct {
    Name string
}

// Structに紐づいた関数Greetが、interfaceのGreetと同じシグネチャ
// UseはGreeter interfaceを実装したものとみなされる
func (u User) Greet() string {
    return "Hello, " + u.Name
}

func PrintGreeting(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    u := User{Name: "Taro"}
    PrintGreeting(u) // User は勝手に Greeter とみなされる
}

アサーション

  • 変数がどの型かを示す構文がちゃんとある。
  • Switch文でパターンマッチ的なこともやれるが、TSほど厳密じゃない。
var x any = "hello"

// okで型がマッチしたかどうかが代入される
// 例外が存在しないGoらしい考え方だ。
s, ok := x.(string)
fmt.Println(s, ok) // "hello" true
// x が取りうる型の「Union」を型制約として定義
type Printable interface {
    ~int | ~string | ~[]int
}

// x は Printable だけに限定
func printType[T Printable](x T) {
    switch v := any(x).(type) {
    case int:
        fmt.Println("int:", v)
    case string:
        fmt.Println("string:", v)
    case []int:
        fmt.Println("slice:", v)
    default:
        fmt.Println("impossible (for now)")
    }
}

GoRoutine

  • これが一番Go言語の強みに見える。
  • チャネルという機構を使って、Pub/Subする。
  • チャネルはスレッドセーフで、同期される。受信しないとルーチンの実行で止まる。
  • selectを使うことで、チャネルのパターンマッチができる。
ch := make(chan int)

// {}()で無名関数を作成し、funcに渡している
// goroutine を作るには「関数を渡す必要」がある
go func() {
    ch <- 10  // 10 を送信
}()

v := <-ch    // 10 を受信
fmt.Println(v)
func fetch(ctx context.Context, ch chan string) {
    select {
    case ch <- "done":
    case <-ctx.Done():
        fmt.Println("canceled:", ctx.Err())
    }
}

Error

  • 例外機構がないから、throwもない。
  • 例外を投げることを禁じることで、何かしらエラーを表現する型を返す文化。ええやん。
  • interfaceだからError()をメソッドで実装して任意の型を表現するのが良さそう。
type error interface {
    Error() string
}

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}

まとめ

  • Goの思想、すこ。例外なんていらん。
  • 関数型にかぶれている僕は全部immutableにしたいときがありますが、Goは並行処理も強いし、レシーバーの値渡しがそれに近いので、基本、ミュータブルでよさげ。
  • Goroutineが最も特徴的な言語仕様だ。なるほどなぁ。並行処理がマストなバックエンドでよー使われるのがわかる。
  • あとは実際にWebアプリケーション作ってみて、API/DBなどの非同期I/Oをどうさばいていくのか、それぐらいかな。

素晴らしいまとめ

このエントリのお陰で更に解像度あがった。

zenn.dev