Closed159

Goを初めて学習する part1

ハガユウキハガユウキ

今日からGoを初めて勉強する。
(昔progateでやったことあるけど、ほとんど忘れた)
GW明けには、そこそこできるようになっていたい。

GW明けまでに到達していたい状態

  • GinkgoでBDDできる。
  • GoのコンテナをDockerfileを使って立ち上げる。
  • openAPIでAPIドキュメントを定義できる
  • net/httpパッケージを使って、HTTPサーバーを作る
  • APIサーバーからMySQLサーバーに接続できるようにする
  • CRUD操作のエンドポイントを実装できる
  • Go Modulesとは何かを人に説明できる
  • ミドルウェアを理解している
  • 構造体を理解している

余裕があったら取り組みたい項目

  • Goで認証機能を作る
  • ジェネリクスを理解する
  • Goでマイクロサービスを作る
  • Goでクリーンアーキテクチャを実装してみる
  • GoでDDD(レイヤードアーキテクチャか?)を実装してみる
  • Goで実装したアプリケーションをAWSでデプロイする。Terraformでやりたい
ハガユウキハガユウキ

色々予定が重なりアプリケーション作るとこまでは行かなかったが、Goの基本的な仕様はだいぶ理解できた

ハガユウキハガユウキ

とはいえ、Goの基本的なことをいまいち理解できていない気もするので、逆算的なやり方は一旦置いといて、さらっと基礎的な本を読む。
スターティングGoってやつをさらっと読むか。
これは今日から明日にかけて読み切る予定。ほんとさらっと読むので。

ハガユウキハガユウキ

一旦練習がてら、macにGoをインストールしてみる
(ほんとはローカルを汚したくないからコンテナでやった方が個人的には良いけど)

ハガユウキハガユウキ

このサイトでGoをインストールできる。
https://go.dev/dl/

インストールしたGoは、/usr/local/goディレクトリにインストールされる。

/usr/local/go/bin/go version
go version go1.20.4 darwin/amd64

このbinディレクトリには、バイナリファイル(要は実行ファイル)が置かれているそう。
binはバイナリの頭文字のbinってことか。
https://linuc.org/study/knowledge/544/

あと、ざっくりした理解になっちゃうけど、
コマンドを実行する→コマンド名と同じファイル名のバイナリファイルを実行するという認識で多分良さそう。
毎回フルパスで入力してコマンド実行するのはめんどいから、ファイル名だけ入力してバイナリファイルを実行できるようにしないとな。そのためには、PATHを通す必要があるな。PATHを通せば、そのファイル名のファイルが、指定したパスの配下にないかをコンピュータが探してくれる。PATHを通せばコマンド名(ファイル名)だけでバイナリファイルを実行できる
https://qiita.com/trunkatree/items/df8adf7a01302b741225

ハガユウキハガユウキ

envコマンドでPCの環境変数の一覧を確認できる。
ただ、これだと見づらいので、echoコマンドで環境変数の値を表示する。設定した環境変数は、環境変数名の前に$をつけて参照することができる。
ちなみに、echoコマンドは文字列や環境変数の値を出力するコマンド。

echo $PATH

こんな感じでパスを通す。exportコマンドは環境変数を設定する際に必要らしいんだけど、なぜつける必要があるのかはいまいち分からず。とりあえず、ここはあんま重要ではないので、とりあえず環境変数に値を設定する際につける必要があるものと知っておけばいいか。

.zshrc
export PATH="/usr/local/go/bin:$PATH"

その後、ターミナルでsource ~/.zshrcを実行して、goコマンドを実行すると、ちゃんと実行できることが確認できた。また、echo $PATHで、goのバイナリファイルが格納されているディレクトリのファイルパス(/usr/local/go/bin)が環境変数PATHにちゃんと設定されていることが確認できた。

go version
go version go1.20.4 darwin/amd64
ハガユウキハガユウキ

Goは、外部ライブラリが格納されているディレクトリの場所を知るために、環境変数GOPATHを利用するそう。(この環境変数が未設定の場合は、一部のGoのツールを利用できない)

ハガユウキハガユウキ

とりあえず、GOPATHは設定した。
vimのノーマルモードで/[文字列]で文字列検索できるの初めて知った。

ハガユウキハガユウキ

よし、Goの環境構築は終了したので、今からgoを書いてくか。
VSCodeでGoを書くので、Goの拡張機能だけインストールした。

ハガユウキハガユウキ

Goの拡張機能インストールしたけど、めちゃVSCode遅くなったので、やっぱ消すか。

ハガユウキハガユウキ

TS使いからすると、Goのimport文にfromを書かないのにすごく違和感を感じるけど、パッケージ自体をインポートしているって意味なのか。TSだと、あるパッケージのある関数だけインポートしてくるからfromって書き方が自然なんだけど、Goだと、パッケージ全体をインポートするのが普通の書き方なのね。

ハガユウキハガユウキ

Goでは、変数や関数といったプログラムのすべての要素は、何らかの1つのパッケージに属している。
なので、自分でプログラムを書く際も、まずはパッケージの宣言を書く。
1つのファイルには単一のパッケージについてしか書けない。
(ややこしいが、単一のパッケージが複数のファイルで構成されるとかはOKである)

あと、Goだとセミコロンを書かないので、そこはTSというかRubyに似ている。
(コンパイラが文の末尾と判定した場合、暗黙的にセミコロンを挿入してくれる)
ただ、関数の書き方はTSと似ているな。

hello.go
// パッケージの宣言
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, World")
}

main関数の定義はGoプログラムのエントリーポイント(実行が開始される場所)である。
Goプログラムのエントリーポイントは、mainパッケージの中に定義された関数mainであると定められている。

ハガユウキハガユウキ

go runコマンドで直接プログラム実行できた。
goはコンパイルを前提としたプログラミング言語だけど、go runコマンドを使うことで、ビルドプロセスを隠蔽しつつ、直接プログラムを実行できる。

go run hello.go
// => Hello, World
ハガユウキハガユウキ

ビルドとコンパイルの違いがいまいちわかっていなので、調べた。

  • コンパイル
    • ソースコードをバイナリに変換すること
  • ビルド
    • ソースコードが書かれたファイルから、最終的な実行ファイルを作成すること
    • ビルドはコンパイルとリンクを合わせた処理である。
    • リンクでは、コンパイル後の複数のバイナリファイルやライブラリのファイルの連結処理をしている。

https://nisshingeppo.com/ai/whats-build/
https://wa3.i-3-i.info/diff502programming.html

複数のソースコードのファイルから構成される場合であっても、最終的な実行形式のファイルは1個なのか。だからリンク処理が必要なのか。
https://atmarkit.itmedia.co.jp/ait/articles/1105/23/news128.html

ハガユウキハガユウキ

Goプログラムのビルドは以下のgo buildコマンドで実行できる。
ビルド対象のファイル名を指定する。oオプションで出力する実行ファイルのファイル名を指定できる。
成功すると、hello.goと同じディレクトリにhelloという名前の実行ファイルが作られる。docker buildコマンドも確かこんな感じだったな。

go build -o hello hello.go
docker build -t my-hello-world:latest .

ちょっと違うか。docker buildの場合、-tオプション。これはビルド後のイメージ名(とタグ)を指定するのに使う。

ハガユウキハガユウキ

go buildコマンドを引数を渡さずに実行すると、カレントディレクトリ内にある.goという拡張子のファイルを全てビルド対象とする(エントリーポイントはおそらくmain関数が含まれているファイル)。この時、実行ファイルの名前を指定していないが、指定しない場合、カレントディレクトリの名前が実行ファイル名になる。インポートしているパッケージとかも自動でビルド対象になるそう。多分。
https://www.digitalocean.com/community/tutorials/how-to-build-and-install-go-programsc

ハガユウキハガユウキ

Goでは一つのディレクトリには一つのパッケージしか存在できない。 以下の図の場合、サブディレクトリ(zoo)内にanimalsパッケージがあるってだけで、animalsというサブディレクトリ自体はパッケージではないから、カレントディレクトリには一つのパッケージ(hello.go)しか存在しないで合ってる。

├── README.md
├── go.mod
├── hello
├── hello.go
└── zoo
    ├── animals
    │   ├── elephant.go
    │   ├── monkey.go
    │   ├── rabbit.go
    │   └── sample.go
    └── main.go

この図では、sample.goというsampleパッケージが存在しているので、一つのディレクトリには一つのパッケージしか存在してはいけないという言語仕様を無視している。そのため、buildしようとすると以下のエラーが発生する。

go run zoo/main.go
zoo/main.go:5:3: found packages animals (elephant.go) and sample (sample.go) in /Users/yuuki_haga/repos/go/zoo/animals

sample.goを消せば普通に動く。
https://devlights.hatenablog.com/entry/2019/08/15/053236#Go言語では一つのディレクトリの中には一つのパッケージしか存在できない

ハガユウキハガユウキ

go buildコマンドでmainパッケージのmain関数を含むファイルを指定してビルドをすると、実行ファイル名はmain関数を含むファイル名と同じになる。

ハガユウキハガユウキ

実行ファイルだから、以下のようにファイル名を指定すれば実行できる。
go runで実行した場合と同様の出力が得られることが確認できた。
goの実行ファイルのバイト数はめちゃくちゃ大きい。ls -alで確認すると分かる。
なぜかというと、実行ファイルにGoのランタイム本体と指定したパッケージの機能全てが含まれている為である。GoはOSによって提供される標準的なライブラリにさえ依存しないので、このように実行ファイルに色々含めている。

./hello
Hello, World

ランタイムについてあんまよくわかってないけど、とりあえず、「実行時に必要になるあれやこれやの部品・環境」って理解に一旦留めておくか。

https://zenn.dev/hsaki/books/golang-concurrency/viewer/gointernal

このランタイムが実行ファイルに組み込まれているから、JavaやC#のような各プラットフォームの差異を吸収する仮想マシン(実行環境固有のバイナリに変換するソフトウェア)を用意しなくても良いのかなと思った。
なので、Goプログラムをビルドした結果の実行ファイルは、何の準備も必要なく、そのまま実行環境上で動作できる。
https://e-words.jp/w/JVM.html

ハガユウキハガユウキ

Goだとそういやインデント4つだった。忘れてた
RubyもTSもインデント2つなので、Goを書く際には気をつけねば。

ハガユウキハガユウキ

Goで複数パッケージをインポートする際にセミコロンかかんのか。
そこは違和感あるな。

import (
    "fmt"
    "./animals"
)
ハガユウキハガユウキ

GOPATHって非推奨だったんか、、知らんかった笑

Go では GOPATH という特殊な概念があって、Go のコードはライブラリも含めてすべて $GOPATH/src 以下に置くという約束になっている

確かに、$GOPATH/src配下にパッケージ置いてないよみたいなエラー出たな。
https://qiita.com/propella/items/e49bccc88f3cc2407745#go-get

Goのパッケージ管理はGo Modulesがスタンダードだそう。
Go Modulesは、go.modファイルとgo.sumファイルを用いて依存パッケージのバージョン管理を行う仕組み。
goコマンドのサブコマンドとして提供されているので、Goをインストールした時点から利用できる。
Go Modulesは、2018年にリリースされたGo1.11で試験的にサポートが始まり、2019年にリリースされたGo 1.13から正式にサポートされた。
$GOPATH/src配下にパッケージのディレクトリを置かなくても、OKになる

ハガユウキハガユウキ

Go Modulesかどうかは、go.modがあるかないかで判断される。
go mod initが失敗する場合、モジュール名を指定すれば解決する。
モジュール名は、github.com/go-practice/module_nameのような感じが一般的なのかと思った。ユニークにする必要がある

Go Modulesのモードになっている場合、インポートする際のパスの指定方法が
(go.modに記載したモジュール名) + (go.modのある場所からのパッケージの相対パス)になるので注意。
https://qiita.com/fetaro/items/31b02b940ce9ec579baf

ハガユウキハガユウキ

モジュール名は公開プロジェクトであれば、 github.com/my-account-name/my-repo-name 等とするのが一般的ですが、非公開プロジェクトであればドメイン形式を取らなくても良いです (例: my-project-name/my-library-name)。

モジュール名は公開プロジェクトなら、github.com/my-account-name/my-repo-nameが一般的なんだ。なるほどね。公開されるモジュール名が世界で一意である必要があるため
https://maku77.github.io/p/t269cgj/

https://zenn.dev/shellyln/articles/b2992891f8f3f9381025#1.-b.-これから始めるプロジェクトでは、最初から新しいパッケージ管理システムである-go-modules-を使う

ハガユウキハガユウキ

Goにおけるパッケージとモジュールの違いがあやふやになってたのでまとめる

  • パッケージ

    • パッケージとは、1つのディレクトリにまとめられたソースファイルの集まり
    • 1ディレクトリには1パッケージしか存在できない。
    • よくあるサンプル だと、カレントディレクトリにmain.goが配置されている。これはちゃんと1ディレクトリに1パッケージしか存在できないという言語仕様を満たしている。
    • 特に理由がなければ、ディレクトリ名とパッケージ名は合わせておくと分かりやすい。
  • モジュール

    • Goにおけるモジュールとは、一緒にリリースされた関連するパッケージの集合体のこと。
    • つまり、複数のパッケージの集合体である。
    • 一般的にはGoリポジトリには、1つのGoモジュールがリポジトリ直下に格納されている。

モジュールの命名規則だったり、モジュールとリポジトリの関係などの理解がまだ曖昧なので、いずれ深ぼるか
https://www.twihike.dev/docs/golang-primer/packages
https://maku77.github.io/p/t269cgj/
https://blog.framinal.life/entry/2021/04/11/013819

ハガユウキハガユウキ

Goではパッケージに定義した変数や関数をエクスポートしたい場合、名前の最初の1文字目を大文字にする。特にエクスポートする予定がないなら、小文字で良い。
パッケージをインポートする場合、エクスポートされた名前のみを参照することができる。エクスポートされていない名前は、パッケージの外からアクセスできない。
TSの場合エクスポートを書く。
Rubyの場合は、特にエスポートとか書かなかった気がする。
https://go.dev/tour/basics/3

ハガユウキハガユウキ

同じパッケージに定義した関数は、package_name.関数名()のようにしなくて良い
普通に関数名()で呼び出せる。
当たり前っちゃ当たり前か。

ハガユウキハガユウキ

mainパッケージを複数のファイルで定義した場合、単一のgoファイルだけビルド対象とすると、そのgoファイルしかビルド対象としないので、エラーが起きる。その場合はmainパッケージを構成する全てのgoファイルを指定すれば良い。それかワイルドカードを使う。

  go run main.go app.go
  
  or 
  
  go run *.go
ハガユウキハガユウキ

_test.goで終わっているファイルを、パッケージをテストするためのファイルとしてGoは扱う。
testingという標準パッケージをインポートすると、テストできる。
animalsディレクトリ配下にanimals_test.goファイルを作成する。

package animals

import (
    "testing"
)

func TestElephantFeed(t *testing.T) {
    expect := "Grass"
    actual := ElephantFeed()

    if expect != actual {
        t.Errorf("%s != %s", expect, actual)
    }
}

go test 対象パッケージのディレクトリでテストを実行できる。
カレントディレクトリがパッケージのディレクトリなら、特に何も指定せずに、go testのみでテストを実行できる。

ハガユウキハガユウキ

Goにおける全ての変数は型を備えている。
変数の型は大きく分けて以下の3種類

  • 値型
    • 値そのものを格納するための型
  • 参照型
    • 「スライス」、「マップ」、「チャネル」という3つのデータ構造のいずれかを格納するための型
  • ポインタ型
    • ポインタを格納するための型
ハガユウキハガユウキ

Goには、パッケージ変数とローカル変数の2種類に分かれる。
(定義される場所によって違うだけ)

任意の関数の中に定義された関数がローカル変数。
関数定義の外側の定義された変数がパッケージ変数(パッケージに所属する変数)。

パッケージ変数はパッケージ全体で共有できるので、いろんなところで変更ができる。
そのため、無闇に使わない方が良いかも

ハガユウキハガユウキ

GoはTSの変数(例えばconst)のように、再代入の制限がない。
Goで定数を定義したいなら、constで定義する。

ハガユウキハガユウキ

変数定義には明示的な定義と暗黙的な定義がある。

package main

import (
    "fmt"
)

func main() {
    i := 1
    fmt.Printf("数字: %d\n", i); // => 数字: 1
    i = 2
    fmt.Printf("数字: %d\n", i); // => 数字: 2
}
ハガユウキハガユウキ

goではセミコロンを省略できる(goの場合、省略するのが一般的)

ハガユウキハガユウキ

goのvscode拡張、今やったらインストールできて正常に起動できた。
こいつをインストールすると、変数にホバーすると型が見えるので良いな
絶対インストールしないといけないやつだった笑

ハガユウキハガユウキ

Goでは型をキャストすることができる。
(明示的にやる必要があるが)

TSの型アサーションと比較されがちだが、TSの方はTSコンパイラに型情報を教えているだけで、本当に型が変化したわけではない(なので、TSの型アサーションはキャストではない)。
TSの数値型はnumberしかないので、型を変換するってことをあんましないのかも。
TSで型をキャストしたいって思ったことがあんまないので。

整数リテラルを使って暗黙的に定義した変数はint型に定まることがわかる。
今回は、int型を符号なし整数型のuint64型にキャストしようと思う。

    i = 2 // iの型はint
    u := uint64(i) // uの型はuint64
ハガユウキハガユウキ

ラップアラウンドとは、処理可能な範囲の最後に達した後に、最初に戻ること。
Goではbyte型はuint8型の別名として定義されている。このbyte型はサイズが3ビットなので、2の8乗で256通りのビットの組み合わせがある。つまり、符号なし(unsigned)なので10進数に変換したときに最大値は255である。

余談だが、符号なしは0と正の数。符号ありは負の数、0、正の数をカバーしている。

https://e-words.jp/w/符号なし整数型.htmln

ハガユウキハガユウキ

Goではオーバーフロー(桁あふれ)が発生した時に、その演算結果をラップアラウンドさせる。
256をint型で定義して、byteに変換すると、ラップアラウンドして0に戻る。

    m := 256
    b := byte(m)
    fmt.Printf("数字: %d\n", b); // => 0
ハガユウキハガユウキ

GOPATHのデメリット、Go Modulesの良さがいまいち分かっていないので、進めていく中で理解していきたい

ハガユウキハガユウキ

Goでは、整数型の演算によってオーバーフローする可能性を考慮しつつ、プログラミングを行う必要がある。
Goではオーバーフローするとラップアラウンドするので、x < x + 1が必ずしも常に真であるとは限らない。

mathパッケージ(数学関連の機能を含むパッケージ)に非実装依存である各整数の型の最小値や最大値の定数が定義されている。
mathパッケージの定数を利用して、整数のオーバーフローに対応した防御的なプログラミングを行うことが、ラップアラウンドの大佐食うとなる。

func doSomething(a, b uint32) bool {
    // 普通にa + bの不等式を作ると、オーバーフローしてラップアラウンドしてしまうので、引き算の不等式にしている。
    // 蓬莱ならa + bで最大値より大きな数になって惜しいのだが、そのまま不等式にすると、オーバーフローしてラップアラウンドしてしまう
    if (math.MaxInt32 - a) < b {
	return false
    } else {
        return true
    }
}
ハガユウキハガユウキ

Goの浮動小数点型には、float32型とfloat64型がある。

浮動小数点とは

「数字を『X × Y ^ Z(XかけるYのZ乗)』形式で表現するやり方、もしくは、仮数部と指数部のみを覚えておく小数の扱い方なんだな
https://wa3.i-3-i.info/word14959.html

浮動小数点リテラルを使用した暗黙的な変数定義では、float64に型が定まる。

    // var f float64
    f := 1.1

ハガユウキハガユウキ

Goにはstring型がある
string型はTSと一緒やな。
ダブルクォートで囲った文字列リテラルを代入すると、string型が割り当てられる

ハガユウキハガユウキ

Goの配列定義はRubyやTSに比べるとちょっと特殊。
[a, b, c]を変数に代入するって感じではない。
最初に型名を定義して、型名に続けて、{ }で囲ったブロックに要素の初期値を指定する。

// ts
// tsの場合、型推論されるので、型アノテーションはいらない。
 const a: number[3] = [1, 2, 3];
// go
    a := [4]int{1, 2, 3, 4}
    fmt.Printf("配列: %v", a) // => 配列: [1 2 3 4]

Goの配列の型とTSの型って書き方結構違うな。
Goは[4]intで、具体的な要素数を指定しないといけない。そして、鉤括弧を要素の型の前に書く必要がある。
TSはnumber[]で具体的な要素数を指定しなくて良い。そして、鉤括弧は要素の型の後ろに書く。
また、TSの配列はconstで定義しても拡張可能だけど、Goの配列型は拡張や縮小は不可能。サイズは常に固定。ある要素に代入して値を書き換えるとかはできるけど。
Goで可変長配列のような柔軟なデータ構造を使いたいなら、スライス(Slice)をというデータ構造を使う。

ハガユウキハガユウキ

%vはさまざまな型のデータを埋め込むことができる。配列型とかを%dとかで表現できないので、%vを使う。
%Tは、データの型情報を埋め込むために使う。

ハガユウキハガユウキ

配列に初期値を与えない場合、ブロックを書かなければ良い。
要素数が指定されているので、代わりに0だったりfalseだったりから文字だったり、値がないことを表す値が入っている。

ハガユウキハガユウキ

配列型の代入で気をつけるべき点は、配列型の代入では全ての要素のコピーが発生する。配列型の変数にある配列型の値を代入すると、その値を参照するのかと思いきや、その値のコピーがメモリ上に作られ、そのコピーされた値を参照する

    var (
        a1 = [3]int{4, 5, 6}
        a2 = [3]int{1, 2, 3}
    )

    na2 = a1
    fmt.Printf("配列a2: %v\n", a2) // => 配列a2: [4 5 6]

    a2[0] = 0
    a2[2] = 0

    fmt.Printf("配列a2: %v\n", a2) // => 配列a2: [0 5 0]
    fmt.Printf("配列a1: %v\n", a1)  // => 配列a1: [4 5 6]
ハガユウキハガユウキ

今考えると、変数を別の変数に代入しようとするとコピーが発生するのは当たり前だから、なんでこのとき参照すると思っていたのだろうか。

ハガユウキハガユウキ

Goには、interface{}型というものがある。{}まで含めて一つの型の名前である。
interface{}型は全ての型と互換性を持つので、どんな値でも代入できる。
interface{}型の初期値はnil
Goにおけるnilは具体的な値を持っていない状態を表す特殊な値。nullと同じようなもの。

TSにおけるanyのような型だが、any型の変数のように演算に使うことはできない

ハガユウキハガユウキ

Goの論理演算子は、仕組み的にはTSと同じ。
しかし、Goの論理演算子ではオペランドに論理型の値しか指定できないので自由度が低い
(TSでは何でもオペランドにできる。オペランドがthruthy or falsyに判定される)

ハガユウキハガユウキ

Goはオブジェクト指向機能を持たない。
なので、関数の定義と構造体型を定義することが、プログラミングにおける中心的な作業となる。

また、Goには「構造体」と「関数」の関係を明確にするための「メソッド」と呼ばれる特殊な関数の形式がある。しかし、これはオブジェクト指向プログラミング言語におけるメソッドとは意味合いが異なるので注意する

ハガユウキハガユウキ

Goにはvoid型が存在しない。
Goで関数を定義する際には必ず戻り値の型を書く必要があるが、
Goで戻り値の型がvoidの関数を定義したいなら、戻り値の型定義を省略すれば良いだけ。
(TSでは戻り値の型はそもそも省略できるので書かないことが多い)

func hello() {
    fmt.Println("Hello")
}
ハガユウキハガユウキ

Goには例外機構が存在しない。そのため、関数を呼び出した際にエラーが出たかどうかは戻り値で判定する。Goでは関数が複数の戻り値を返すことができるという特性を利用して、エラーが発生したかどうかを戻り値の一部で示す。(TSでもタプルで複数の戻り値を返すことはできるけど、あんま使わない。Reactのカスタムフック作る時にオブジェクトの戻り値を返すけど、あれのいいところは分割代入で値を取得するので、使う側の人間が順序を気にしなくても良いところ)

result, err := doSomething()

if (err != nil) {
    // エラー処理
}

変数errで受け取っている二番目の戻り値は、エラーの発生の有無を表している
(変数名errも慣例的な決まり事だそう。)
このような書き方はGoにおけるよくある書き方であるそう。今はどうなんだ?

ハガユウキハガユウキ

GoではTSと一緒で関数を値として扱うことができる。
GoにはTSと同じで無名関数という関数の定義方法がある。
これは関数リテラルで表現できる。Goの関数定義の名前を書いてないバージョン。

    f := func(a, b int) int { return a + b }

    fmt.Printf("値: %d\n", f(2, 5)) // => 値: 7
    fmt.Printf("型: %T\n", f) // => 型: func(int, int) int

TSのアロー関数を使うと、これくらいのコードならすごいシンプルに書けるから、やっぱTSの表現力ってすげーなって感じる。まあ、あんまGoを知らないってのもあるからもっと良い書き方があるのかもだけど。あと、型を出力する際に%T的なのがTSにはないから、そこはGoすげーなって思う。
(typeofで型がわかると言えばわかるけど、詳細な型が分からない)

// addNumberの型は (a: number, b: number) => number
// Goの関数の型は、func(int, int) intなので、引数名が省略されている
const addNumber = (a: number, b: number) => a + b;
console.log(`値: ${addNumber(2, 5)}`);
ハガユウキハガユウキ

Goにおけるクロージャとは、状態をもつ無名関数である
クロージャとは、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。

package main

import (
	"fmt"
)

func later() func(string) string {
     // 1つ前に与えられた文字列を保存するための変数
    var store string
     // 引数に文字列をとり文字列を返す関数
    return func(next string) string {
        s := store
        store = next
        return s
    }
}

func main() {
    f := later()

    fmt.Println(f("Golang")) // => ""
    fmt.Println(f("is")) // => "Golang"
    fmt.Println(f("awesome")) // => "is"
}

昔なんかの記事で見たけど、無名関数内でstoreを参照しているから、storeがメモリに残り続けるって言ってた気がする。なので、この挙動が実現できる。

Goのゼロ値について調べた。

変数に初期値を与えずに宣言すると、ゼロ値( zero value )が与えられます。
ゼロ値は型によって以下のように与えられます:
数値型(int,floatなど): 0
bool型: false
string型: "" (空文字列( empty string ))

つまり、Goの変数は必ず初期化され、初期値を指定しない場合、ゼロ値が初期値になるのか。
TSの場合、undefinedになるだけだからな。Goのこの挙動は意外だった

let a: string
console.log(a); // => undefined

https://go-tour-jp.appspot.com/basics/12
https://qiita.com/tenntenn/items/c55095585af64ca28ab5

ハガユウキハガユウキ

Goにおけるスコープは大きい単位から順に
パッケージ > ファイル > 関数 > ブロック > 制御構文(if, for等)

ハガユウキハガユウキ

パッケージで定義されている識別子を参照するには、foo.MAXのように、パッケージ名と識別子を.(ドット)で繋ぐ。これはfmtパッケージや自作パッケージで何回かやった気がする。

ハガユウキハガユウキ

パッケージ名を省略するインポートめっちゃ良いなって最初の頃は思っていたけど、パッケージにどんな関数が定義されているかを事前に知っていないといけない(定義元を見にいくのがめんどくさい。パッケージを書けば補完機能が働いてどんな関数が定義されているかわかる)かったり、名前の衝突が発生することがあるのか。
確かに、パッケージ名を省略するインポートを使う場面ってあんまないかもな。

ハガユウキハガユウキ

ファイルのスコープ

パッケージ定義が複数のファイルによって構成される場合、import宣言は各々のファイル内でのみ有効になる。ファイルが分割されていても、定数や変数、関数については、参照が可能。インポート宣言のみ独立していることに注意する。

ハガユウキハガユウキ

Goではループを記述するために用意されている構文はforのみ。すごいシンプル笑

ハガユウキハガユウキ

GoとTSのif文の違い

  • TSの条件部分は、truthy or falsyで判定される為、比較的自由に指定できます。Goの場合、truthyとfalsyの概念がおそらくないため、条件部分には論理型の値もしくは、論理型の値を返す式や関数呼び出しを指定します。
  • TSでは条件部分を丸括弧で囲みますが、Goでは囲みません
ハガユウキハガユウキ

go fmtでソースコードを標準のフォーマットに整形することができる。これはめちゃくちゃ便利やな。本来ならsave時にやって欲しいけど、色々競合するのがめんどくさそうなので一旦保留

ハガユウキハガユウキ

for文をチラッと見たけど、範囲節によるforは結構使いそう。
範囲式は予約後rangeと任意の式を組み合わせて定義する。
iはループごとに増加して、sにはiに対応する配列要素が代入される。
配列の要素数 - 1まで繰り返される。
この範囲式と組み合わせたforは配列専用ではなく、複数の要素を保持する性質を備えるデータ型に対して使える。配列型、配列へのポインタ、スライス、文字列、マップ、チャネル等。

package main

import (
    "fmt"
)

func main() {
    fruits := [3]string{"Apple", "Banana", "Cherry"}

    for i, s := range fruits {
        fmt.Printf("fruits[%d]=%s\n", i, s)
    }
}

/*
fruits[0]=Apple
fruits[1]=Banana
fruits[2]=Cherry
*/
ハガユウキハガユウキ

Goのswitch文で条件式に比較演算子を使えるの初めて知った。
もし比較演算子を使う場合は、switchの隣に書く変数を書かないのだけど、TSだと確かtrueって書いてた気がする。Goでも明示的にtrueと書いているのと同じらしい。

    switch a {
		case 1:
			fmt.Printf("%d\n", a)
		default:
			fmt.Println("no hit")
		}

		b := 2
    switch {
		case a == 1 && b == 2 :
			fmt.Printf("%d\n", a + b)
		default:
			fmt.Println("no hit")
		}
ハガユウキハガユウキ

Goには式によるswitchと型によるswitchがある。
式によるswitchが上の例で。型によるswitchは、変数の型に応じて処理を分岐できる

ハガユウキハガユウキ

interface{}型の実際に代入された型を特定するためには、Goにおける型アサーションを使う。
TSの型アサーションは型を断定するものだけど、Goの型アサーションはinterface{}型の実際に代入された型を特定するためのGoの機能。x.(T)がチェックするための構文。Tには任意の型を指定する。

型アサーションが成功した場合、一番目の変数にはその値が、二番目の変数にはtrueが代入される。
失敗した場合は、一番目の変数にはその型の初期値、二番目の変数にはfalseが入る

		// tsだと変数名の後ろに;が必要
		var i interface{} = 3

		d, isInt := i.(int)
		e, isFloat65 := i.(float64)
		f, isString := i.(string)

		fmt.Printf("%d, %v\n", d, isInt) // => 3, true
		fmt.Printf("%v, %v\n", e, isFloat65) // => 0, false
		fmt.Printf("%s, %v\n", f, isString) // => "", false
ハガユウキハガユウキ

型によるswitchは型アサーションと組み合わせて書く。

		switch i.(type) {
		case bool:
			fmt.Println("bool")
		case int:
			fmt.Println("int")
		default:
			fmt.Println("dont know")
		}
ハガユウキハガユウキ

書いてて思ったけど、GoにはTSの共用体型みたいなのがないな。

ハガユウキハガユウキ

Goでは実行時に発生させるエラー(ランタイムエラー)のことを「ランタイムパニック」というそう

ハガユウキハガユウキ

ゴルーチンとはGoのランタイムによって管理される軽量な並行処理スレッドのこと。
main()関数も、1つのゴルーチンの中で実行されている。
go文を用いて、任意の関数を別のゴルーチンとして起動することで、処理を並行して走らせることができる
ゴルーチンはGoランタイムによって管理されている。そのため、スレッドやメモリアクセスの管理など複雑な作業を人間がやる必要がなくなる。l

ゴルーチンを実際の現場ではいつ使うかのイメージができていないので、やりながらふかぼる必要があるな。
https://gihyo.jp/dev/feature/01/go_4beginners/0005
https://techblog.zozo.com/entry/improve-digdag-task-with-goroutine#:~:text=ゴルーチンとはGoの,ランタイムが管理します。

ゴルーチンの本でこの本がおすすめだそう。
https://www.oreilly.co.jp/books/9784873118468/

ハガユウキハガユウキ

Goにおける全ての変数は型を備えている。
変数の型は大きく分けて以下の3種類

  • 値型
    • 値そのものを格納するための型
  • 参照型
    • 「スライス」、「マップ」、「チャネル」という3つのデータ構造のいずれかを格納するための型
  • ポインタ型
    • ポインタを格納するための型

参照型の生成には、組み込み関数makeを使用する。
スライス、マップ、チャネルのデータ構造はいずれもmakeによって生成される。
そのため、関数の呼び出し方にはバリエーションがある。

ハガユウキハガユウキ

スライス

  • スライスはGoで最も利用頻度の高いデータ構造。
  • スライスは、いわゆる可変長配列を表現する型。
  • 配列の型に要素数を書かないと、スライスの型になる。
    var s []int
  • スライスのデータ構造を作成するためには、組み込み関数makeを使う。
  • sliceにおけるmakeの場合、makeの第一引数には使いたい参照型の型、第二引数には要素数と容量を指定する。
  • スライスの容量とは、スライスがメモリ上に確保しておく領域のこと。第三引数を指定しない場合、要素数と容量は同じ。
  • スライスはデータ構造としては可変長配列だが、スライスの要素数を超過した要素へのアクセスはランタイムパニックを発生させる
package main

import (
	"fmt"
)

func main() {
    s := make([]int, 10)
    // 初期値を設定していないので、ゼロ値が初期値となる 
    fmt.Printf("%v\n", s) // => [0 0 0 0 0 0 0 0 0 0]
}
  • 組み込み関数lenでスライスの現在の要素数を調べることができる
package main

import (
	"fmt"
)

func main() {
    s := make([]int, 10)
    fmt.Printf("%v\n", len(s)) // => 10
}
ハガユウキハガユウキ

スライスはmakeを使わずに配列型のリテラルと同様の書き方で生成できる。
(配列型との違いはスライスのリテラルの場合、要素数を書かない。スライスの型が要素数を書いていないので当たり前か)

makeの書き方との違いはスライスの容量を個別に指定できないこと。

package main

import (
	"fmt"
)

func main() {
    s := make([]int, 10)
    fmt.Printf("%v\n", len(s)) // => 10
}
ハガユウキハガユウキ

簡易スライス式ってのがあって、配列やスライスをもとにして新たなスライスを生成できるそう。
いつか使いそうだから、言葉だけ覚えとく。

ハガユウキハガユウキ

配列とスライスの最も大きな違いは拡張性。Goの配列は拡張できないけど、スライスはapend関数を使うことで拡張できる。Goの配列は要素数を含めて一つの型を表現するので、[10]intと[11]intは全く別の型として扱われる。

スライスは可変長配列を表現するデータ構造なので、要素数に制限がない。

		s = append(s, 5)
		fmt.Printf("%v\n", s) // => [1 2 3 4 5 7 6]

		s = append(s, 7, 6)
		fmt.Printf("%v\n", s) // => [1 2 3 4 5]
ハガユウキハガユウキ

appendは二番目以降の引数は可変長引数。
スライス変数の後ろに...をつけると、可変長引数の関数にスライスを展開して渡すことができる。
この後ろにつける演算子のことをunpack演算子と呼ぶ。展開するときに使う。
前につけるスリードットはpack演算子と呼び、まとめる役割を持つ。可変長引数を持つ関数を定義したいときとかに使う。unpack演算子は変数につける感じだけど、pack演算子は型につける感じやな。

TSのスプレッド構文が変数の前にスリードットをつけるくせに、Goの意味的にはunpack演算子だからめっちゃややこしい笑
https://zenn.dev/mikankitten/articles/cfa2ef834e338e
https://qiita.com/hnakamur/items/c3560a4b780487ef6065

		s2 := []int{2, 4, 6}
		s3 := []int{3, 6, 9}

		s4 := append(s2, s3...)
		fmt.Printf("%x\n", s4) // => [2 4 6 3 6 9]
ハガユウキハガユウキ

appendを使う場合、必ず:=か=による変数の代入を伴う必要がある。
変数の代入を伴わない関数appendは、コンパイル時にエラーとなる

ハガユウキハガユウキ

可変長引数を持つ関数を初めて作った。
この場合、引数aは可変長配列を受け取る引数なので、aの型は[]int
つまり、引数aはスライスであることがわかる。

// sum(2, 3, 5)で呼び出すと10が返ってくる。
func sum(a ...int) int {
    s := 0

    for _, v := range a {
        s += v
    }

    return s
}

ハガユウキハガユウキ

関数の可変長引数は末尾に一つしか定義できない。
そのため、可変長引数を複数定義したり、可変長引数の後に別の引数を定義したりはできない

ハガユウキハガユウキ

値渡しとは、関数を呼び出す際に、仮引数の値を実引数の値としてコピーすることである。
参照渡しとは、関数を呼び出す際に、仮引数の値のメモリ番地を実引数の値としてコピーすることである

Goで関数の仮引数に配列などの基本的なデータ構造を渡す場合、値渡しが行われる。
しかし、スライスなどの参照型の場合、参照渡しが行われる。これによって、関数内の処理でスライスの値を書き換えることができてしまう。
https://magazine.rubyist.net/articles/0032/0032-CallByValueAndCallByReference.html

ハガユウキハガユウキ

参照型は初期値を定義しない場合、値型と同じでゼロ値が入る。
ただ、ちょっと違うのは、参照型の場合、何かの値の参照を持っているので、今回の場合、何の参照も持っていないので、ゼロ値が何もないことを表すnilになる。これは配列のゼロ値と大きく違う。何も初期化していないスライスの場合、値への参照がないので空のスライスが作成される

    var a [2]int
    fmt.Println(a) // => [0, 0]

    var b []int
    fmt.Println(b) // => []
ハガユウキハガユウキ

Goの参照型の一つであるマップは、TSにおける連想配列のようなものである。
TSの場合、{}を使うけど、Goの場合、[]を使う。
Goにおけるマップとは、任意の型のキーと任意の型の要素のペアを保持できる特殊な配列と覚えておけばOK
Goのマップを表す型は「map[キーの型]要素の型」

ハガユウキハガユウキ
package main

import (
	"fmt"
)

func main() {
    m := make(map[string]string)
    m["name"] = "yuki"
    m["age"] = "26"

    fmt.Printf("%v\n", m) // => map[age:26 name:yuki]
}
ハガユウキハガユウキ

リテラルが用意されているので、参照型の値を生成するのにmakeを使わなくても良い

    m2 := map[string]string{
        "name": "yuki2",
         "age": "26",
    }

    fmt.Printf("%v\n", m2) // => map[age:26 name:yuki2]
ハガユウキハガユウキ

マップ内にスライスやマップのリテラルを書く場合、型の記述を省略して{}だけ書いてもOK

ハガユウキハガユウキ

TSだと配列の存在しない要素にアクセスしようとすると、undefinedが帰ってくるけど、
Goだとマップでゼロ値が帰ってくるので、気をつけないと。値が入っていると思ってしまうな。
(Goだとマップやスライスで参照したときにゼロ値が帰ってくるのかわからんからいつか深ぼらないとな)

ハガユウキハガユウキ

mapの二番目の戻り値には、キーに対応した要素が存在するかを表すbool値が入っている

    v1, ok1 := m3["name"]
    v2, ok2 := m3["age"]
    fmt.Printf("%v, %v\n", v1, ok1) // => yuki, true
    fmt.Printf("%v, %v\n", v2, ok2) // => "", value
ハガユウキハガユウキ

チャネルとは、ゴルーチンとゴルーチンの間でデータの受け渡しを司るためにデザインされたGo特有のデータ構造である。ゴルーチンによる非同期処理を必要としないプログラムでは、原則的に使用する必要がない。

ハガユウキハガユウキ

チャネルの型は chan 型名
チャネルにはサブタイプを指定できて、受信用か送信用かを指定できる。
何も指定しなかったら、受信にも送信にも使えるチャネルの型になる

ハガユウキハガユウキ

チャネルは参照型なので、makeで生成できる。

// 変数chはバッファサイズが20のint型のチャネル
ch := make(chan int, 20)

// チャネルに整数5を送信
ch <- 5

// チャネルから整数値を受信
i := <-ch
ハガユウキハガユウキ

キューのいいところはデータの取り出す順序が保証されているところ

ハガユウキハガユウキ

チャネルにおけるバッファとは、キューのサイズのことである。

ハガユウキハガユウキ

バッファという言葉の意味自体は、データを一時的に保存する場所のこと。

ハガユウキハガユウキ

ゴルーチンとはGoのランタイムによって管理される軽量な並行処理スレッドのこと。
main()関数も、1つのゴルーチンの中で実行されている。
go文を用いて、任意の関数を別のゴルーチンとして起動することで、処理を並行して走らせることができる

ハガユウキハガユウキ

ゴルーチンを使ったプログラムを自分で作ってみた
各ゴルーチンが非同期でどのような順序で動作するかは、タイミングにより異なるため、このプログラムの出力は、実行のたびに変化する。

package main

import (
    "fmt"
    "time"
)

func receive(name string, ch <-chan int) {
    // 何も指定のない裸のforは無限ループ
    for {
        // チャネルからデータを取り出す
        // okはチャネルのバッファ内が空でかつチャネルがクローズされた状態かどうかを表す真偽値
        // チャネルのバッファ内に受信可能であるデータが存在した場合、変数okの値はtrueとなる
        i, ok := <-ch
        if !ok {
            // 受信できなくなったらループ終了
            // break文は最も近い位置のforループを中断させる
            fmt.Println(name + " is done.")
            break
        }
        fmt.Println(name, i)
    }
}

// mainが実行されたとき、mainでチャネルを生成して、ゴルーチンを3つ起動している
// main関数も一つのゴルーチンで実施されている
// go構文を用いて、任意の関数をゴルーチンとして起動することで、処理を並行して走らせている
func main() {
    // 明示的にバッファサイズを指定しない場合、バッファサイズ0のチャネルになる
    ch := make(chan int, 20)

    go receive("1st goroutine", ch)
    go receive("2nd goroutine", ch)
    go receive("3rd goroutine", ch)

    for i := 0; i < 100; i++ {
        ch <- i
    }
    close(ch)

    // ゴルーチンの完了を3秒待つ
    // timeパッケージは時間に関する処理を提供している
    // receive関数がゴルーチンとして起動している間も、main関数は先に進んでしまう。
    // そのため、main関数が終わらないように、receive関数側のゴルーチンの完了を待つために三秒停止している。
    // じゃないと、中途半端に数が出力されてプログラム全体が終了する
    // Go言語にはメインゴルーチンが終了したタイミングでプログラム全体を終了させるという特性がある
    time.Sleep(3 * time.Second)
}
ハガユウキハガユウキ

Go言語には、main関数のゴールーチンが終了したタイミングで、プログラム全体を終了させるという特性がある。
上のコードの場合、main関数を終わらせないためにSleep処理を入れて並行処理が完了するのを待つようにしている。しかし、実施にはこのようなやり方はしなくて、syncパッケージを使う。syncパッケージに関してはおいおいやってく。
https://tech-blog.rakus.co.jp/entry/20220127/go
https://gihyo.jp/dev/feature/01/go_4beginners/0005

ハガユウキハガユウキ

チャネルはクローズとオープンという2つの状態を持っている。
makeで生成したチャネルはオープンした状態から始まる。
しかし、送信処理が完了したチャネルを明示的に閉じることは可能。
close(チャネル名)で、チャネルをクローズできる。
クローズしたチャネルに対してデータの送信を行うとランタイムパニックが発生する。
しかし、ややこしいのだが、クローズというのはチャネルにデータを入れるのがクローズというだけで、チャネルからデータを取り出すのはできる。

ハガユウキハガユウキ

selectは1つの処理の流れで複数のチャネルを処理する場合に使う。
selectを使うことで、チャネルの受信待ちでチャネルの受信待ち処理を書いているゴルーチンが停止してしまい、ゴルーチン内に書いている次のチャネル処理へ行けない問題を解決できる。
selectを使うと、複数のチャネルに対する処理、送信処理ともにゴルーチンを停止させることなくコントロールすることができる。

selectはswitchと同様にはじめに成立したcase節が優先的に実行されるわけではない。
select下の複数のcase節の処理が継続できる場合には、Goのランタイムはどのcase節を実行するかを「ランダム」に1つ選択して処理する。以下の場合、defaultはすべてのcase節の処理の継続が不可能である場合に、実行されるので、このプログラムでは処理が到達することはない。

package main

import (
	"fmt"
)

func main() {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)
    ch1 <- 1
    ch2 <- 2

    select {
    case e1 := <-ch1:
        fmt.Printf("%d: ch1から受信\n", e1)
    case e2 := <-ch2:
        fmt.Printf("%d: ch2から受信\n", e2)
    case ch3 <- 3:
        fmt.Printf("%d: ch3へ送信\n", <-ch3)
    default:
        fmt.Println("ここへは絶対に到達しない")
    }
}
ハガユウキハガユウキ

OOP言語であれば、クラスやオブジェクトの設計や実装がプログラミング作業の中心になる。
Goでは、構造体とインターフェースの設計・実装が重要なポイントになる

ハガユウキハガユウキ

Goにおけるポインタ

  • Goにおけるポインタとは、メモリ上のアドレスのこと。
    • アドレスは値なので、変数に代入できる。
  • アドレスが代入された変数のことを、ポインタ型変数と呼ぶ。
    • ポインタ型を定義するためには、変数のデータ型にアスタリスクをつける。
    • このデータ型には、ポインタで取得する値の型を指定します。
    • :=を使ってポインタ型変数を宣言することもできます。
ハガユウキハガユウキ

変数はコピーされる

変数1 = 変数2 と書いた場合、変数の値そのものがコピーされる。 その後にどちらかの変数の値を変更しても、もう片方の変数は影響を受けない。

ハガユウキハガユウキ

関数の引数とかも同様にコピーされるんだけど、参照型の値を仮引数として渡した場合、参照渡しが行われる。

参照渡しとは、関数を呼び出す際に、仮引数の値のメモリ番地を実引数の値としてコピーすることである。実引数を参照しようとすると、メモリ番地を経由してメモリにある仮引数の値を参照できる
https://magazine.rubyist.net/articles/0032/0032-CallByValueAndCallByReference.html

ハガユウキハガユウキ

参照型を使わないと参照渡しできないのだが、仮引数にポインタを渡せば、参照渡しが実現できる。

func updateNum(a [3]int) {
    a[0] = 7
    a[1] = 7
    a[2] = 7
}

func updateNumOfReference(p *[3]int) {
    (*p)[0] = 7
    (*p)[1] = 7
    (*p)[2] = 7
}

func main() {
    a := [3]int{1, 2, 3}
                 
    // 配列からポインタを生成
 b := &a
 fmt.Printf("配列b: %d\n", *b) // => 配列b: [1 2 3]
 updateNumOfReference(b)
 fmt.Printf("配列b: %d\n", *b) // => 配列b: [7 7 7]
}
ハガユウキハガユウキ

ただし、変数や関数の引数がコピーされる原則において、Goの文字列は例外。
string型の値を変数への再代入や関数の引数として使った場合であっても、文字列の実体が別のメモリ領域にコピーされることはない。

string型の構造体を見れば分かる。string型は内部的には「文字列の実体へのポインタ」と「文字列のバイト長」によって構成される。つまり、string型は、その型の仕組みそのものにポインタを内包している。そのため、文字列の実体そのものが不必要にコピーされてしまうことはない

ハガユウキハガユウキ

アドレス演算子&を使うことで、任意の〇〇型の変数から〇〇のポインタである*〇〇型の値を生成できる。

ポインタ型の変数に入っているアドレスを経由してメモリ上の値を参照するためには、演算子*をポインタ型変数の目に置く。こうすることで、ポインタ型が指し示すデータのデリファレンスをすることができる

デリファレンスとは、ポインタ型が保持するメモリ上のアドレスを経由して、データ本体を参照するための仕組み。

ハガユウキハガユウキ

デリファレンスで参照した値に代入すると、値を書き換えることができる

package main

import (
	"fmt"
)

func main() {
    var i int
    // ポインタpを定義
    p := &i
    i = 5
    fmt.Println(*p) // => 5
    *p = 10
    fmt.Println(i) // => 10
}


ハガユウキハガユウキ
  • 任意の〇〇型の変数から〇〇のポインタである*〇〇型の値を生成したい
    • アドレス演算子&を使う
    • &を変数の前に置く
  • ポインタ型の変数(要はポインタ)に代入されているアドレスを経由してメモリ上の値を参照したい
    • *を使う
    • *をポインタ型の変数(ポインタ)の前に置く
    • そうすると、デリファレンスできる
    • デリファレンスの目的は、値の参照か値の書き換え
    • ポインタに対して*を指定すればデリファレンスができることを覚えておく
ハガユウキハガユウキ

Goではさまざまな型に対してポインタ型を取ることができる

ハガユウキハガユウキ

構造体

  • 構造体(struct)とは、複数の任意の型の値をを一つにまとめたもの
    • 構造体型は、さまざまなデータ構造を一つの型として取り扱うもの。
    • 構造体自身に構造体を含めることもできる
    • Goでは複雑なデータ構造を組み立てるためにさまざまな構造体を定義する必要がある。
      • OOPにおけるクラスの定義が重要であるように、Goにおいては構造体の定義が重要
    • Goにはクラスやオブジェクトなどはない。
      • しかし、任意の構造体と手続きを結びつけるためのメソッドという機能はある。
      • メソッドという名前だが、GoのメソッドはOOPのメソッドとは若干意味合いが異なる。
ハガユウキハガユウキ

GoにもTSの型エイリアス的なのがある。
予約後のtypeを使うのでTSと同じだが、その後の書き方が違う。
Goでは以下の構文である
Goのtypeはすでに定義されている型を元に、新しい型を定義するための機能である

type 新しい型 既存の型
// ts
type MyNumber = number;

const age: MyNumber = 26;
// go
package main

import (
	"fmt"
)

func main() {
    type MyInt int
    var age MyInt = 26
    fmt.Println(age) // => 26
}
ハガユウキハガユウキ

Goでは既存の型が同じでも型名が違うと、互換性がなくて代入とかができないので注意。
(TSだとそんなことはないのだがGoだとそのような挙動がある)

ハガユウキハガユウキ

構造体は、struct {フィールドの定義}で定義できる。
structで定義された構造体には、typeで新しい型を定義するのがtypeのよくある使い方

ハガユウキハガユウキ

構造体は値型の一種である。
そのため、構造体型の変数を定義すると、構造体に定義されている各フィールドに必要なメモリ領域が確保され、それぞれのフィールドは型に合わせた初期値(つまり、ゼロ値)を取る。

構造体のフィールドは、構造体型の変数.フィールド名で参照できる。
構造体のフィールドには演算子=を使って値を代入できる

ハガユウキハガユウキ

ここまでの理解だと、構造体ってTSのオブジェクト型に似ているな。

ハガユウキハガユウキ

構造体を使ってコードを書いてみる

package main

import (
	"fmt"
)

type User struct {
	Name string
	Age int
}

func main() {
		var u User
		fmt.Println(u.Name) // => ""
		fmt.Println(u.Age) // => 0

		u.Name = "yuki"
		u.Age = 26

		fmt.Println(u.Name) // => "yuki"
		fmt.Println(u.Age) // => 26
}
ハガユウキハガユウキ

構造体型に各フィールドの初期値を指定しつつ、構造体を生成するための複合リテラルが用意されている。

    // 定義した構造体型を利用して、構造体を生成している
    r := User{Name: "yuki", Age: 26}
    fmt.Println(r) // => {yuki 26}
ハガユウキハガユウキ

Goの慣習で構造体のフィールド名は、先頭が英大文字の英数字によるフィールド名が好ましい

ハガユウキハガユウキ

構造体のフィールドに自身の型を含めるような再起的な定義はできんのか。。
TSのクラスだと、クラスを定義しつつ型も定義されるから、クラス定義内で型を利用することができるんだけど、構造体だとダメなのか

type User struct {
	Name string
	Age int
	User // コンパイルエラー
}
ハガユウキハガユウキ

てか、structをtypeと使っている時点で、structで定義したものは構造体型ってことか。

つまり、structは構造体型を定義するためのもので、
structを用いて生成した値が構造体ってことか。

ハガユウキハガユウキ

Goにおけるメソッド

  • Goにおけるメソッドは、OOP言語によくあるメソッドとは違う
  • Goのメソッドは、特定の型に関連づけられた関数
    • OOP言語ではクラスに対してメソッドを定義する。
    • Goでは型に対してメソッドを定義する
  • メソッド定義では、関数とは異なり、funcとメソッド名の間にレシーバの型とレシーバ名が必要になる。
    • この*Point型の変数pがレシーバを表している。
    • 型に定義されたメソッドは、レシーバ.メソッド()という形式で呼び出すことができる
    • なぜ、レシーバをポインタにしているかというと、参照渡しでレシーバに直接影響を与えたい為。レシーバはポインタであるべきという決まりも特にはないが、基本的な原則では「構造体に定義するメソッドのレシーバはポインタ型」にすべきらしい。
package main

import (
	"fmt"
)

type User struct {
	FirstName string
	LastName string
	Age int
}

func (p User) FullName() string {
    return p.FirstName + " " + p.LastName
}

func (p *User) updateAge(age int) {
    (*p).Age = age
}

func main() {
		u2 := User{FirstName: "yuki", LastName: "hoge", Age: 26}
		fmt.Println(u2.FullName()) // => yuki haga
		fmt.Println(u2.Age) // => 26

		(&u2).updateAge(27)
		fmt.Println(u2.Age) //=> 27
}
ハガユウキハガユウキ

同一のパッケージ内に引数や戻り値が異なる同名の関数を複数定義することはできない。
しかし、メソッドの場合、レシーバの型さえ異なっていれば、同名のメソッドを定義できる

ハガユウキハガユウキ

型のコンストラクタ

  • GoにはOOPにおけるコンストラクタはない
  • しかし、型のコンストラクタというパターンを慣例的に作っている
  • 以下の例では構造体型Userと構造体の初期化のための関数NewUserを定義している
  • 型のコンストラクタを表す関数はNew型名のように命名するのが一般的
  • 型のコンストラクタは対象の型のポインタ型を返すように定義するのが望ましい
package main

import (
	"fmt"
)

type User struct {
	FirstName string
	LastName string
	Age int
}

func (p User) FullName() string {
    return p.FirstName + " " + p.LastName
}

func (p *User) updateAge(age int) {
    (*p).Age = age
}

func NewUser(FirstName string, LastName string, Age int) *User {
	  // 今回は使わないが、newは指定した型のポインタ型を生成するための組み込み関数
		// 複合リテラルは2種類の書き方があるが、フィールド名を明示的にしないと、値が順番に割り当てられてしまい、ミスに気づきづらい。
		// TSとかRubyだと、プロパティ名と値の変数名が同じ時、プロパティ名しか書かなくても良いんだけどね。Goにはなかった。
    return &User{
        FirstName: FirstName,
        LastName: LastName,
        Age: Age,
		}
}

func main() {
		// Rubyのようにキーワード引数で指定できないのもなんとかしたいな。型が一致していることでしか保証できていないので、
		// その値が本当にその型に割り当てるべきなのかまで保証できていない
		// TSでも基本Goみたいな感じで関数を定義するんだけど、複雑な型を関数に渡す場合、オブジェクトで渡すから、
		// その値が本当にその型に割り当てるべきかが担保されている。
		// オブジェクトはオブジェクトリテラルで簡単に記述できるから、オブジェクトリテラルやっぱええな
		fmt.Println(NewUser("yuki", "hoge", 26))
}
ハガユウキハガユウキ

そうか、型のコンストラクタを定義することで、ゼロ値による初期化を防ぐことができるのか。
しかし、複合リテラルを使えば同様に初期化もできるし、フィールを明示的に書けば値がそのフィールドに入ることを保証できるので、コンストラクタよりそっちのがよくね?と思ってしまう。

ハガユウキハガユウキ

型のコンストラクタの利点は、初期化時に初期化とは別に行いたい処理を利用者から隠蔽できること

ハガユウキハガユウキ

なんでポインタを返すのかなと思ってchatGPTに聞いてみた(あくまで参考程度に)。コンストラクタの戻り値は変数に代入する。この際にコピーが発生する。構造体とかそういうメモリコピーのコストが高そうな値を変数にコピーするとメモリ効率が悪い。そのため、戻り値をポインタで返す。

一点疑問なのが、関数呼び出しが完了したら、関数内で定義された変数はGCによってメモリ内から削除される。そのため、戻り値でポインタを返したら参照できなくてエラーになるのでは?と思った。
(Goのランタイムが自動的にGCを実行する)
chatGPT曰く、ポインタを介して関数内で生成されたオブジェクトは、関数のスコープを超えて使う場合、オブジェクトが使用され続ける限りメモリ上に存続するそう。この場合メモリ解放はプログラマーの責任だそう(本当か?)

ハガユウキハガユウキ

レシーバの型.メソッドのように書くと、メソッドを関数として利用することができる

ハガユウキハガユウキ

Goのメソッドは、呼び出し側のレシーバの型が値型だろうがポインタ型だろうが呼び出せる。
とはいえ、混乱を招くので、定義側でレシーバの型をポインタ型として定義しているなら、呼び出し側のレシーバの型はポインタ型にする

ハガユウキハガユウキ

Goだと言語仕様にEnumがない。
擬似Enumも想像しているのとちょっと違う

ハガユウキハガユウキ

パッケージの外部のパッケージから参照したいなら、エクスポートすればいいので大文字にすればいいのか。

ハガユウキハガユウキ

Goの構造体には「タグ」という機能があり、フィールドにメタ情報を付与することができる

ハガユウキハガユウキ

Goにおけるインターフェース

  • Goにおけるインターフェースとは型の一種であり、任意の型がどのようなメソッドを実装すべきかを規定している。
  • このインターフェイスには、こういったメソッドがある。とだけ宣言している型とも言える
  • インターフェースは予約後interfaceを使って定義する。interface { メソッドのシグネチャの列挙 }という形式。

シグネチャってなんやねんって思って調べた

プログラミングの分野では、関数やメソッドの名前、引数の数やデータ型、返り値の型などの組み合わせのことをシグネチャという。
https://zenn.dev/t_kitamura/articles/90bc98a3787044

ハガユウキハガユウキ

やっとインターフェースがわかってきた気がする。
インターフェースって言葉の本来の意味は、2者を繋ぐもの。その部分とGoを絡めて説明している記事がなかなかなくて腑に落ちなかった。
つまり、Go言語においてインタフェースを定義する理由は、ある型(以下、型Aとする)がある関数(以下、関数Aとする)に直接依存しないようにする為である(おそらくそう)。Goにおけるインターフェースは2者間の依存を防ぐ型。
型Aを受け取る関数Aの中で型Aのメソッド処理を書くと、関数Aは型Aに依存してしまう。これの何が良くないかというと、

  • 型Aと関数Aが直接依存することによって、関数Aでは使用しないメソッドに対しても関数Aが依存してしまう。これは不必要な依存である。
  • 型Bを引数として関数Aを呼び出したいってなった時に、関数Aは型Aに依存しているので、関数Aに型B用の処理を追加する必要がある。型Bかどうかを関数A内で判定する処理を追加した結果、関数Aは関数Aを使う側のファイルに書くべきである処理を持ってしまい、関数A内の記述がすごく複雑になって再利用性のない関数になってしまっている。そして関数Aは型Bにも依存しているので、型Bに変更が起きた際に、一個変更したいだけなのに、いろんなところを変更しないといけず、変更コストがめちゃ高くなる

関数A内の処理でインターフェースを経由してメソッドにアクセスすることで、型と関数の直接的な依存から、インターフェースを経由した依存関係になる。そのため、上記2点の問題点も解決される

  • 関数A内ではインターフェースを経由して型にアクセスするので、インターフェースに定義していないメソッドにはアクセスできない。つまり、不必要な依存を防げる
  • 関数A内ではインターフェースを経由して型にアクセスするので、型Bに対してもインターフェースを実装すれば、型Bでも関数Aが使える
  • 型に別のメソッドを定義しても、関数に影響がない
  • また、インターフェースを定義することで、型にメソッドの実装を強制することができる。

https://www.sunapro.com/go-interface/
https://cloudsmith.co.jp/blog/backend/go/2021/08/1847845.html

ハガユウキハガユウキ
  • Interfaceが期待するメソッドをすべて満たした型には、暗黙的にInterfaceが実装される。 Goでは、TSのimplementsのようにこのクラスはインターフェースを実装してますよというコードを明示的に書かなくて良い。型に対してインターフェースが期待するメソッドを全て実装した場合、暗黙的に型に対してインターフェースが実装される
  • Interfaceを満たした変数はinterface型として扱うことができるので、Interface型の変数に代入することができる

https://thiscalifornianlife.com/2021/01/10/golang-interface/
https://zenn.dev/yuki_tu/articles/8def6851273424
https://cloudsmith.co.jp/blog/backend/go/2021/08/1847845.html

ハガユウキハガユウキ

インターフェースを理解したことで、interface{}型も理解できる。

interface{}型とは要するに「実装すべきメソッドが一つも定義されていない」インターフェースである。特別扱いされた特殊な方ではなく、要は空のインターフェースを表す型である。空のインターフェースは実装すべきメソッドがないので、int型や[3]floac64型など全ての型はinterface{}型と互換性があるとうことになる

ハガユウキハガユウキ

名前が「S」から始まる関数のグループは,文字列(String)を 「生成」 するために使います
この関数単体では標準出力などはできないため,通常のコード内では変数などにデータを格納するためなどに使います

なるほど、fmt.Sprintfは生成した文字列を変数に格納したいときに使うのか。わかりやすい。
https://zenn.dev/mkosakana/articles/97466310534b4c#fmt.sprintln

ハガユウキハガユウキ

インターフェース実装した

package main

import (
    "fmt"
)

// 文字列化できることを示すインターフェース
// インターフェースを経由して型に定義してあるメソッドを利用するので、
// インターフェース自身はメソッドを呼び出すことで、どんな処理が実施されるのかを知らない(てか知らなくて良い)
// (メソッドの処理に関しては型の都合の処理も含まれるので、インターフェースは実装を知らなくて良い)
// そのため、実装した型ごとに、同じ名前だけど独自の処理を持つメソッドを呼び出せる
type Stringify interface {
    ToString() string
}

type Formatter interface {
    Format() string
}

type User struct {
    Name string
    Age int
}

// *Userという型に対してToStringメソッドを定義した
// つまり、*Userに対してインターフェースを実装したので、Userに対してインターフェースを実装したわけではない
func (u *User) ToString() string {
    return fmt.Sprintf("名前: %s, 年齢: %d", u.Name, u.Age)
}

func (u *User) Format() string {
    return fmt.Sprintf("名前: %s(%d)", u.Name, u.Age)
}

type Car struct {
    Name string
    Price int
}

func (c *Car) ToString() string {
    return fmt.Sprintf("名前: %s, 値段: %d", c.Name, c.Price)
}

func main() {
    user := User{Name: "yuki", Age: 26}
    car := Car{Name: "bmw", Price: 20000}

    // *User型に対してインターフェースを実装しているので、
    // *User型の値は、interface型(つまり、Formatter型)として扱える
    fmt.Println(format(&user)) // => 名前: yuki(26)

    a := [2]Stringify{&user, &car}

    for _, v := range a {
        fmt.Println(v.ToString())
    }
    /*
	名前: yuki, 年齢: 26
	名前: bmw, 値段: 20000
    */
}

// f.ToString()を書くとエラーが起こる
// つまり、インターフェースを利用することで、利用しないメソッドへの依存を防げているということが分かる。
func format(f Formatter) string {
    return f.Format()
}

この記事めちゃ参考になった
https://qiita.com/greenteabiscuit/items/b48233e9d92fa20be378

このスクラップは2023/05/08にクローズされました