社内勉強会に参加してGoに入門した。
環境
- Mac (M1)
- VSCode (Goの拡張機能を入れる)
- Go 1.19 (研修は1.17でビルドする)
Windows環境は作るのMacより面倒らしい。
研修の資料は社外秘なので、やったところと同じ範囲の A Tour of Go を参考にする。
HelloWorld
おなじみの入門儀式である。
適当に作ったディレクトリに hello.goファイル作って、ソースをコピペする。
// hello.go
package main // これはmainパッケージです!と高らかに宣言
import "fmt" // fmtモジュールを使いたいという気持ちの表れ
// mainパッケージのmain関数はプログラム実行時最初に叩かれると決まっている
func main() {
fmt.Println("Hello, World") // fmtモジュールのPrintln関数でテキスト出力
}
この時点で既にJSとの文化の違いを感じる🤔 けど似てる部分もある。
hello.go
と同じルートで go mod init hello
をターミナルで実行すると、go.mod
ファイルが作成される。
これはJSで言うところのpackage.json
のようなもので、構成とかが保存される設定ファイルである。
中身はそのままGoなのでとてもスッキリしている。
// go.mod
module hello
go 1.17
go run .
で実行できる。
ディレクトリ構成
研修だと演習問題を1つずつこなしていく形式で、1つ親のディレクトリ作ってその中に演習問題ごとのディレクトリとモジュールを作成するのがおすすめ、とあったので、JSのノリでディレクトリ構成を分けたらエラーになったw
つまりこうしたかった。
/go-study
/01_hello
go.mod
hello.go
/02_printf
go.mod
printf.go
// 01_hello/go.mod
module hello
go 1.17
// 01_hello/hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}
Workspace Mode
Go 1.18 以上であれば、ワークスペースモードを利用できるので、
ルートに go.work
ファイルを追加して、use
関数で各モジュールを設定しておく。
(各ディレクトリの go.mod
バージョン指定は1.18以上にしなければならい)
// go.work
go 1.19
use (
./01_hello
./02_printf
)
これだとルートで go run hello
のように各 go.mod
で指定したモジュール名を利用して実行できる。
VSCode Workspace
バージョン1.18以下だとワークスペースモードは使えないので、VSCodeのワークスペース設定でディレクトリを追加していくことにした。
全て同じ命名でいいし、ディレクトリ複製がしやすいから、演習こなしてくのには良さそう。
// GoStudy.code-workspace
{
"folders": [
{
"path": "./01_hello"
},
{
"path": "./02_printf"
}
],
"settings": {}
}
各ディレクトリで go run .
を実行すればおk。
モジュールの追加
Goでモジュールを追加する場合、それが公式のものであれば自動でやってくれるが、
ユーザーが開発したものは get
コマンドによるインストールが必要。
go get github.com/sirupsen/logrus
不要になったときはremoveするのではなくて、ソースから削除してから tidy
を叩く
go mod tidy -v
JavaScriptと違うGoの文化について
JS使いがハマりそうなところは大体同じなんじゃないか説。
ダブルクォートとシングルクォート
Goでもダブルクォートはstringだが、シングルクォートはrune型になる。
JSで日常的にシングルクォート使ってると、めちゃくちゃ打ち間違える。
命名における大文字と小文字の違い
関数や構造体の名前の先頭が、
大文字の場合:パプリック(外部パッケージから参照できる)
小文字の場合:プライベート(同じパッケージ内からしかアクセスできない)
となる。
これはpublicとか宣言書いた方が目が滑りにくいと思うけどなあ🤔
変数はvar
宣言の呪文は var
である。 最近JSだとvar使わなくなってるので懐かしさを覚える。
カンマ区切りで羅列できるし、型も指定できる。
package main
import "fmt"
var c, python, java bool // この変数全部bool型
func main() {
var i int // int型の変数
fmt.Println(i, c, python, java)
}
varで宣言しつつ変数に値を代入するときもカンマ区切りが使える。
その変数の型は代入した値のものに設定されるので、型の宣言は省略して良い。
package main
import "fmt"
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
// > 1 2 true false no!
挙動はJSとそう変わらんけど、省略しすぎると読みづらくないか?🤔
関数の中に限っては、 :=
を利用することで var
を省略することができる。
package main
import "fmt"
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
VSCode上だと色分けされるんだけど、
やたら省略する書き方って目が滑るからあんまり好きじゃないなあ。
定数はconst
const
で宣言すると定数となる。必ず const
という呪文が必要で、 :=
では宣言することができない。
定数で利用できる型は、文字(character)、文字列(string)、boolean、数値(numeric) のみである。
定数として宣言した変数には代入することができない、っていうのは他の言語と同じ。
package main
import "fmt"
const Pi = 3.14
func main() {
const World = "世界"
fmt.Println("Hello", World)
fmt.Println("Happy", Pi, "Day")
const Truth = true
Truth = 1 // [error] cannot assign to Truth (untyped bool constant true)
fmt.Println("Go rules?", Truth)
}
値が数値の定数は、定数宣言時に型が指定されていなければ状況によって必要な型を取る。
package main
import "fmt"
const (
// Create a huge number by shifting a 1 bit left 100 places.
// In other words, the binary number that is 1 followed by 100 zeroes.
Big = 1 << 100
// Shift it right again 99 places, so we end up with 1<<1, or 2.
Small = Big >> 99
)
func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
return x * 0.1
}
func main() {
fmt.Printf("Type: %T Value: %v\n", Small, Small)
fmt.Printf("Type: %T Value: %v\n", needInt(Small), needInt(Small))
fmt.Printf("Type: %T Value: %v\n", needFloat(Small), needFloat(Small))
fmt.Printf("Type: %T Value: %v\n", needFloat(Big), needFloat(Big))
}
自動で初期値入れてくれる
変数を宣言するとき何も値を入れなかった場合、型に合わせた初期値が自動的に設定される。
これは普通に便利だと思った。
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
fmt.Printf("%v %v %v %q\n", i, f, b, s)
}
// > 0 0 false ""
数値の型多すぎぃ!
上記のruneもそうだけど、Goにおける型の種類見てると、見慣れないものがいろいろある。
// 論理値型
bool
// 文字列型
string
// 符号付整数型
int int8 int16 int32 int64
// 符号なし整数型
uint uint8 uint16 uint32 uint64 uintptr
// uint8 の別名
byte
// int32 の別名(Unicode のコードポイントを表す)
// 文字列にうっかりシングルクォート使うとこれになる
rune
// 浮動小数点型
float32 float64
// 複素数型
complex64 complex128
数値の型めっちゃあるな…🤔
package main
import (
"fmt"
"math/cmplx"
)
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
// 変数 Tobe は bool 型で値は false
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
// 変数 MaxInt は unit64 型で値は 18446744073709551615
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
// 変数 z は complex128 型で値は (2+3i)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
Goのint64といったでかい数値をJSで受け取る場合、numberではなくBigIntになって死ぬ。
型変換
型を関数のように使うことで型変換ができる。
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
なら string(i)
で文字列にできるかというとできなくて、
intをstringにするといった変換はモジュールの利用が必要なようだ。
package main
import (
"fmt"
"strconv"
)
func main() {
var x int64 = int64(9223372036854775807)
var y int = 123
var sx = strconv.FormatInt(x, 10) // int64 to string
var sy = strconv.Itoa(y) // int to string
fmt.Printf("Type: %T Value: %v\n", x, x)
fmt.Printf("Type: %T Value: %s\n", sx, sx)
fmt.Printf("Type: %T Value: %v\n", y, y)
fmt.Printf("Type: %T Value: %s\n", sy, sy)
}
関数
パラメーターと戻り値に型の指定が必要である。TypeScriptに似ている。
型は変数の後に指定する。同じ型の場合は省略も可能。
package main
import "fmt"
// xとyはint型、戻り値もint型です
func add(x int, y int) int {
return x + y
}
// xとyが同じ型なので省略ができる
func add2(x, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
戻り値が複数ある場合はカンマ区切りで並べる。
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
戻り値に名前をつけることで、returnに変数を並べ書くことを省略できる。
naked returnとかいうかっこいい名前がついているが、何をreturnしているのが探さなければならないような、複雑な関数で使うべきものではない。
package main
import "fmt"
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
fmt.Println(split(17))
}
エラー処理
Goはtry-catchのような例外処理を持たないので、関数内でエラーが発生したときにerror型を返す戻り値が設定されている。エラーが発生しない場合errorにはnil
が入るというところで判別ができる。
func main() {
s := "cat"
num, err := strconv.Atoi(s)
if err != nil {
fmt.Println("数値に変換できませんでした")
}
fmt.Printf("%d \n", num)
}
関数によってはセンチネルエラーという、Errから始まる定数が設定されている場合があり、どういったエラーなのか種類の判別が可能である。
未使用のための変数
Goは使ってないモジュールや変数、関数なんかがあるといちいちエラーで怒られる。
ゴミが残らないのでいいんだけども🤔
関数が戻り値を2つ返すが、呼び出し側では片方しか使わない。という場合は結構ある。
// errorは使わん…
int, error := strconv.Atoi("10")
使わないことが決まっている変数は、アンダーバーにすれば怒られなくなる。
// errorをアンダーバーに変更
int, _ := strconv.Atoi("10")
ifとfor文は括弧が不要
中括弧 { }
は必要。
// for文
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
// if文
if x < 0 {
return true
}
while文は省略したforである
forは初期化と後処理も省略できるが、セミコロンすら省略できてしまう。
Goにはwhileがないのでforを使う。
sum := 1
for sum < 1000 {
sum += sum
}
ループ条件すら省略可能なので、無限ループがお手軽に作れる。
for {
}
Switch文はbreakしなくていい
Goのswitch文は上から下にcaseで指定された条件を評価し、当てはまったらそこしか実行しないので、breakは不要である。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
Switch文のcaseは定数や整数じゃなくてもいい
条件式とかも使える。これはもうif文の省略形と思った方がいいのかも🤔
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("When's Saturday?")
// todayはtime.Weekday型でGo playground上だと値はTuesday
today := time.Now().Weekday()
switch time.Saturday {
case today + 0:
fmt.Println("Today.")
case today + 1:
fmt.Println("Tomorrow.")
case today + 2:
fmt.Println("In two days.")
default:
fmt.Println("Too far away.")
}
}
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
遅延実行
deferステートメントを使うと、呼び出し元の関数が終わるまで実行を遅延させることができる。
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// hello world
たくさん defer
すると、全部スタックし、最後にdeferへ渡したものから順に実行される。
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
// > counting done 9 ... 0
使い所がイメージできんのだけどPromiseのようなもんかな?🤔
クラスという概念がない
継承とか移譲のようなクラスにまつわるあれこれもない。
その代わり構造体というものがある。type
と struct
がその宣言である。
// ねこの名前、種類、毛色、年齢を表す構造体
type cat struct {
name string
type string
color string
old int
}
構造体を利用するときは必ず初期化が必要ある。
myCat := cat{} // 変数がそれぞれの型の初期値で初期化される
myCat := cat{ name: "たま", type: "雑種" } // 値を指定して初期化
……どうみてもクラスやないか?🤔
インターフェース
クラスはないが、インターフェースはある。
インターフェースに定義されてるメソッドを実装しなければならない、という縛りもPHPなんかと同じ。
type AutoFeeder interface {
start()
stop()
}
大規模になってくるとこういう縛るの欲しくなってくるもんね。