Open17

Go

yuitayuita

スクリプト言語とコンパイル言語の違い

スクリプト言語
正式な定義は存在しない。
主な言語はPHP、JavaScript、Ruby、Pythonなど
インタプリタ方式(プログラムを一行ずつ機械語に翻訳する方式)によってコンパイルされる。
コードの編集後、すぐに結果が確認できる。
実行の都度変換作業が行われるので、実行速度は遅め。

コンパイル言語
主な言語はC、Java、C#、Goなど
実行する前に全てのコードを一括でコンパイルする。
そのため、動作確認するにはコード変更後にコンパイルの作業が必要。
(GoをDocker環境で開発する場合、変更の都度コンテナを立ち上げるのは面倒なので、ホットリロードを行うairなどのパッケージを使うことが多い。)
機械語に変換された状態で実行されるので実行速度は速い。
コンパイル時にエラーがあればコンパイルできないので、実行前にエラーが発覚しやすい。

GoはCの高速性とスクリプト言語の手軽さを両立させる目的で設計されている。

yuitayuita

クロスコンパイルが可能
クロスコンパイルとは、開発環境とは異なるOSやアーキテクチャで動作するプログラムを生成できること。
例えば、macOSからWindowsやLinuxで動作するプログラムを作成できる。

yuitayuita

並行処理
ゴルーチンを用いた並行処理が可能。

yuitayuita

標準ライブラリや周辺ツールの充実
汎用性の高いライブラリが標準で用意されている。周辺ツールも充実しており、特にgofmtという標準フォーマッターが用意されているので、コーディング規約などを設ける必要がない。

yuitayuita

ガベージコレクターを備えている
ガベージコレクターとはプログラムが動的に確保したメモリ領域のうち、不要になった領域を自動的に解放する機能のこと。

yuitayuita

go run コマンドの役割
Goはコンパイル言語だが、go run コマンドはビルドプロセスを隠蔽して直接プログラムを実行してくれている。
go run main.go とするとmain.goファイルだけが対象となり、他にファイルがあっても無視される。
その場合go run *.goとするか、対象ファイル全てをコマンドに入力する必要がある。
go buildコマンドについては対象を指定しない場合カレントディレクトリにある全てのGoファイルを対象に取る。

yuitayuita

Goの変数や関数といったプログラムの全ての要素は何らかのパッケージに属する
1つのファイルに記述できるのは1つのパッケージだけ。
1つのファイルに複数のpackage宣言があるとコンパイルエラーが発生する。
Goは不要な宣言を許可しない。コードの清潔さを保つためや、ビルド時間の削減、依存関係の複雑化を防ぐため。
インポートされたパッケージが使用されていないとコンパイルエラーが発生する。
パッケージ名の前に_を入れればコンパイルエラーは一応回避できる。

yuitayuita

Goはファイル構成に決まりはなく、管理しやすく見通しのいい形であれば自由に構成できる。
パッケージ名がそのままディレクトリ名になるように構成を行うのがスタンダード。

yuitayuita

変数
Goの変数は大きく分けると値型、参照型、ポインタ型の3種類に分かれる。
値型
整数や実数といった値そのものを格納する
参照型
スライス、マップ、チャネルの3つのデータ構造のいずれか指し示す
ポインタ型
ポインタを表す

ポインタ
値や関数といったメモリ上の実体をそのアドレス値によって間接的に表現するもの。
メモリのアドレスのこと。

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

// int型の変数hogeを定義して1を代入
var hoge int
hoge = 1

暗黙的(型推論)

// 同上
hoge := 1

複数の変数への代入をまとめることも可能

var hoge, huga int
hoge, huga = 1, 2
// この場合hoge = 1, huga = 2となる

演算子:=は変数の代入を行っているのではなく、変数を定義しているので代入はできない。

i := 1
i := 2
// コンパイルエラーになる

明示的に定義する場合も多重定義はコンパイルエラーになる。

var i int
var i int
// コンパイルエラー

原則として同じ変数を複数回定義するとエラーが発生する。

変数定義の際は可能であれば暗黙的な型定義を利用したい。
静的型による整合性の保証の代わりに煩雑な型指定が必要という静的型付け言語のデメリットを軽減するためにも積極的に使って開発効率を上げていくのがいい。

関数内に定義された変数はローカル変数、関数外で定義された変数はパッケージ変数(多言語のグローバル変数に近い)と呼ぶ。

Goは定義した変数が使用されていない場合もコンパイルエラーが発生する。
ここにもGoの特徴が現れていると言えるのでは。

yuitayuita

関数
基本的な定義方法

func [関数名] ( [引数の定義] ) [戻り値の型] {
  [関数の処理内容]
}

戻り値の無い関数、複数の戻り値を持つ関数、名前を持たない関数(無名関数)など色々定義できる。

// 戻り値なし
func hoge() {
  fmt.Println("hoge")
  return
}

// 戻り値複数
func div(a, b int) (int, int) {
	sum := a + b
	dif := a - b
	return sum, dif
}

// 無名関数
f := func(x, y int) int {return x + y}
f(2, 3)

戻り値は_を使って破棄することも可能。
無名関数は関数そのものを値として表現していると言える。

Goは例外機構が無いのでエラーハンドリングは自分で実装する必要がある。
関数が複数の戻り値を返すことを利用して基本的に以下のような形で実装する。

result, err := doSomething()
if (err != nil) {
  // エラー処理
}

クロージャー
GoDocより
クロージャは、Goの無名関数がその周囲にアクセスできるようになったときに発生します。そうすると、それ自身のユニークな状態を保持できるようになります。そして、その状態は、関数の新しいインスタンスを作成するときに分離されます。

要は関数内で状態を保持する機能。
サンプルコード

// Go は、 [_無名関数 (anonymous functions)_](http://en.wikipedia.org/wiki/Anonymous_function)
// をサポートするため、<a href="http://en.wikipedia.org/wiki/Closure_(computer_science)"><em>クロージャ (closures)</em></a>
// を作れます。無名関数は、名前をつけずに
// インラインで関数を定義したい場合に便利です。

package main

import "fmt"

// この `intSeq` 関数は、 `intSeq` の中で定義した
// 無名関数を返します。返された関数は、変数 `i`
// を _閉じて (closes over)_ クロージャを作ります。
func intSeq() func() int {
	i := 0
	return func() int {
		i++
		return i
	}
}

func main() {

	// `intSeq` を呼び出した結果 (関数) を `nextInt` に代入します。
	// この関数は、自分自身の `i` の値をキャプチャし、その値は
	// `nextInt` を呼び出すたびに更新されます。
	nextInt := intSeq()

	// `nextInt` を何回か呼び出して、クロージャの効果を見てください。
	fmt.Println(nextInt())
	fmt.Println(nextInt())
	fmt.Println(nextInt())

	// 関数ごとに個別の状態をもっていることを確認するために、
	// もう 1 つ新たに作成して試してみてください。
	newInts := intSeq()
	fmt.Println(newInts())
}

この場合状態が保持されるのはintSeq()内のi

nextInt内ではi++する無名関数が実行されて、intSeq()のiが1ずつ足されていく。

intSeq()を別の変数(newInts)に代入すると、
そちらは別の状態が保持されるので、最後は1が出力される。

yuitayuita

参照型
参照型にはスライス、マップ、チャネルが定義されている。
スライスにはappendで要素をたせるが配列には足せない。(配列は要素数が変わると別物の型として扱われるので当然といえば当然)
スライスの全要素についてのループ処理にはrangeと組み合わせた範囲節によるforを使うのがいい。

yuitayuita

構造体とインターフェース
ポインタはメモリのアドレスと型の情報のこと。

デリファレンスとはポインタ型が保持するメモリ上のアドレスを経由してデータ本体を参照する仕組みのこと。
Goではstring型の要素のポインタの取得はできないようになっている。一度生成された文字列に変更を加えることは基本的にできないように設計されているため。(絶対ではないが危険とされている)
Goの文字列は不変。

配列のポインタ型は簡潔に記述できるようになっており、以下の場合だと(*p)[i]のような書き方をしなくてもいい。
このように特別扱いされるのはGoの特徴の一つ。Goは合理的であれば一貫性にあまり強く拘らないため。

a := [3]string{"apple", "banana", "cherry"}
p := &a
fmt.Println(a[1]) // "banana"
fmt.Println(p[1]) // "banana"
p[2] = "grape"
fmt.Println(a[2]) // "grape"
fmt.Println(a[2]) // "grape"

構造体は複数の任意の型の値を1つにまとめたもの。
typeを使って型エイリアスを定義した場合、元の型が同じでも型エイリアス同士は互換性を持たない。
構造体は値型の一種。
構造体を関数の引数として渡す場合は基本的にポインタを渡す(値型なのでポインタを渡さないと値渡しになって元の構造体に干渉できない)。

yuitayuita

GoogleのGoスタイルガイド
スタイルの原則(重要度順)

1、明確さ: コードの目的と理論的根拠が読者にとって明確です。
2、単純さ: コードは可能な限り単純な方法で目的を達成します。
3、簡潔さ: コードの S/N 比は高くなります。
4、保守性: コードは保守しやすいように記述されています。
5、一貫性: コードは、より広範な Google コードベースと一貫性があります。

明確さ
明確さは主にわかりやすい命名、役に立つコメント、合理的なコード構成によって実現される。
コードを書く目線ではなく、読む目線で実装するのが重要。
Goは元々、コードが何をしているのかが比較的分かりやすいように設計されている。
不明確な場合や、コードを理解するために前提知識が必要な場合は、コードの目的を明確にするために
以下のような点を意識するといい。

・わかりやすい変数名を使用する
・追加のコメントを加える
・空白やコメントを使ってコードを分割する
・よりモジュール化するために、コードを別の関数やメソッドにリファクタリングする
一律のアプローチはないが、Goのコードを開発する際には明確さを優先することが重要。

単純さ
上から下に向けて読みやすいコードを意識する。
極力標準ツールを用いて実装する。サードパーティライブラリは便利な面もあるが、不要な機能が含まれていたり、無駄な依存関係を産むこともある。コードが複雑になる原因にもなる(複雑にするのは簡単。シンプルにするのは難しい)。
複雑さを必ずしも避けるべきというわけではないが、何か複雑さを追加する場合はそれに応じたコメントによる解説などを入れるべき。
賢いコード(ロジカルなコード)は複雑になりがち。少しコードが長くなってもいいのでぱっと見で何しているかわかるような単純なコードを書くことを心がける。

簡潔さ
コードの簡潔さを妨げる原因には以下のようなものが挙げられる
・繰り返しのコード
・余計な構文
・不透明な名前
・不必要な抽象化
・空白
特に反復的なコードを書くとどこが違うのか判断しにくいのでコメントなので補完する。
コメントする際は重要なところだけが目立つようにして、不要なところにコメントを書かない。
自己流の書き方も他の人に理解されにくので避ける。

保守性
今後改修されることを見越してコードを書く。
適切なテストを書く。
依存関係を最小限に抑える。
仕様していない機能は書かない。
将来の変更や拡張を意識したコードは多少複雑になっても後々役に立つことが多いので良いトレードオフ。
重要なコードは明示的な書き方(コメントするのもあり)を意識して隠れるのを避ける。

一貫性
特別な理由がない場合は一貫性を優先する。
狭い範囲の一貫性よりも広い範囲の一貫性を重視する。

Goは行を分割して書かない。行が長いと感じた場合はリファクタリングを検討する。十分にリファクタリングできている場合はそのままにする。