この記事はディップ株式会社 Advent Calendar 2023の11日目の記事になります。
こんにちは!
2023年の4月にディップ株式会社に入社し、現在Goを使って開発をしている@imkaoruです!
社会人としては9ヶ月目に突入しましたが、日々初めてのこと・わからないことの連続で、なんかずっとバタバタしています。
生まれて初めて師走を実感している今日この頃です!
by Renee French
はじめに
さて、今回は「これから学び始める人へ伝えたいGoの性質と特徴」というテーマで書いてみようと思います。
自己紹介で少し触れましたが業務ではGoで開発しており、私は7月に現在の部署に配属されてから学び始めました。
正直なところ最初から興味があったわけではなく、入社後研修で久々にReactを触ったことでTypeScriptに興味を持ち、私生活ではそっちの勉強を優先していたほどです。
その上、Goには言語特有の難解さがあるように感じられ、学び始めの理解に苦戦していました。
ただ、Goを学んでいるうちに「小さな言語」という性質を知り、自分に合っていると感じる場面が多くあり、気づいたら好きな言語となっていました。
(これがどういうことかは「Goの性質」で説明します。)
まだまだ私自身学び始めの最中ですが、少なくとも最初の理解の難しさで敬遠してしまうのはとても勿体ない言語だと思うので、特にGoをこれから学ぶ方に向けて書いていきたいと思います。
Goの魅力がひとつでも伝わりましたら幸いです。
Goの性質
ここからの構成について、大きく以下の二つに分けています。
- 性質: 設計思想的な部分やGoのいいなって思うところ ← 今ここ
- 特徴: 学び始めに理解を難しくさせていたこと
どんな言語なの?
「初めてのGo言語」という書籍では「過去50年の歷史に学び、今後50年使えるシステムを作るために設計された言語」と表現されています。
設計哲学としては、シンプルで読みやすく、保守しやすい言語を目指し、様々なところからコンセプトを借りてきた実用的な言語、だそうです。
「プログラミング 分類」などで検索すると、「手続き型 ↔︎ 関数型」などの軸に様々な言語を分類している表をよく見るかなと思います。
私は初めての言語に触れるとき、最初にここら辺を見てざっくり特徴を確認しがちなのですが、上述の理由から、Go言語を無理にこのような分類に当てはめて考えるとちょっとややこしくなりそうだと感じています。
C言語との関係は?
C言語の生みの親であるケン・トンプソン(Ken Thompson)氏が共同開発者の一人であることからか構文(for
など)や一部標準関数(fmt.Println
など)の書き方が似ています。
また、コンパイル言語なのでエントリー関数としてまずmain
が呼ばれたりする部分も似ていたりしますが、大局的には全く別の思想を元に開発された言語だといえそうです。
そのため、ぱっと見は取っ付きにくい印象を感じますが、意外とそうでも無いです(Cと比べると)。
↓ 私の最初の印象
by Renee French
ただ、Cよりは限定的ですがコードの中でポインタを使う場面はあります。
しかしGoにはガベージコレクタがあるためメモリ管理の手間が格段に増えるわけではないですし、ポインタに関する演算などの機能はないためやはり意外と怖くない印象です。
(Goでのポインタの使用場面について後ほど少し記載します。)
イディオムとは?
Goの記事などを見ているとよく「イディオム的な」解決策、などイディオムという言葉を目にします。
イディオムとは、コードをより効果的に動作させ かつ 理解しやすくするための標準的な慣習や文化的な規約を指します。
Goでは、言語自体の設計哲学や開発者コミュニティの共有された価値観に基づいてイディオムが形成され、Goのコミュニティ全体でも強く受け入れられている印象です。
Goは、「シンプルで読みやすく、保守しやすい」ことが重要視されているため、冗長さとトレードオフで可読性を大事にしたイディオムが多いように感じます。
イディオムは形式的な規約や強制力のあるものではないため、開発者が採用するかどうかは自由ですが、これらの慣習に従うことでコードの一貫性が高まり、他の開発者との協力がしやすくなるということが最大のメリットのようです。
以下に、イディオム的なことの例を一部紹介します!
- スネークケースは使わず、キャメルケースを使う
- 変数のスコープが狭ければ狭いほど短い変数名をつける
- 型とそれに付随するメソッドの定義は同じファイルに置く
- パッケージ用に長いコメントを書くなら、
doc.go
ファイルを作ってそこに置く - (こんなのも?の例としては、)
fmt.Plintln
系の関数は実は戻り値を返しているが、無視する
(他にもたくさんあるため、全て取り入れるのではなく自分の所属するチームの状況を見て適宜よいものを取り入れていく形がベストなのではないかと思います!)
チーム開発との相性は?
Goの大きな開発目標の一つに「大規模なチームによる共同開発を容易にする」があります。
先ほど取り上げたイディオムもそうですが、この開発目標に関するもう一つ大きな要素として、言語開発チームによって「厳選された機能が最小限組み込まれている」ことが挙げられます。
ここについて、(少々過激ではありますが)初めてのGo言語では以下のように述べています。
言語の専門家が自分のアイデアの素晴らしさを実証するために組み込んだような最新の機能を使わなくても、素晴らしいシステムは作れると思うのです。
そのためGoには、多くの言語では使える機能がそもそも実装されていなかったりします。
しかし、それがこの言語の良さだと思います。
私が感じた1番のメリットは、同じ処理の書き方に、人による差が出にくいことです。
レビューする際など、とりあえずそのコードが何をしているかはわかりやすいので、実装の仕様理解だけに集中でき、レビューコストが比較的低いかもしれません。
また、「字下げにタブを使用する」や「条件文の{
の位置が決まっている」などフォーマットに標準仕様があったり、宣言されたローカル変数は全て使っていることが保証される(使っていないものがあるとコンパイルエラーになる)こともごちゃごちゃせずにすみ、チーム開発と相性がいいといえるのではないかと思います。
一旦まとめ
再掲ですが、「過去50年の歷史に学び、今後50年使えるシステムを作るために設計された言語」ってとても安心感があると感じます。
Goは2012年にリリースされてから、変わらずメジャーバージョンが 1 のままであり、まだ1度も破壊的変更がされていません。
後方互換性を重視し、アップデートは小規模なものになる傾向があるようです。
詳しくは、Go 1 と Go プログラムの将来を読んでみてください!
また、ちょっと余談ですが「コードは理解しやすくなければならない」の原則で有名なリーダブルコードの内容とも基本的に相性がいい言語だなーと思います!
では続きまして「Goの特徴」に移ります!!
Goの特徴
先ほど構成で書きましたが、次は他の言語と比較して「学び始めに理解を難しくさせていたこと」を書きます。
前提として、業務で扱ったことある言語はGoだけです。そのため、学生時代に触れていたC, Python, JavaScriptと比較しています。
また、一つ一つしっかり書くと長くなってしまい、一旦ざっくりと認識してほしい!の目的から逸れてしまうと思い、深く書いていない部分も多いです。
リフレクションやジェネリクス等そもそも記事の中で触れていないことも同様の理由からです。
変数宣言について
Goは静的型付け言語なので 型と初期値を設定する
var x int = 10
型を省略して書いた場合は、リテラルのデフォルトの型となる
var x = 10 // この場合、10は整数リテラルなのでint型となる
省略して書ける理由は、コンパイル時の型推論が備わっているため
関数内では var
を使わず、:=
を使って宣言するのが普通
x := 10 // var x = 10 と同じ
関数の外の変数宣言はパッケージレベルの変数となるため(できるだけ)避ける
代入するリテラルのデフォルトの型と希望する変数の型が異なる場合は、型変換ではなく var
を使って宣言するのがイディオム的
var x byte = 50 // 型変換(x := byte(50) のようにも書ける)で書くよりも明示的でよいとされる
複数の変数の宣言を1行で行うことは、複数の値を返す関数からの戻り値を代入する時だけにとどめ、基本的には控える
xResult, yResult, err := exampleFunc(x, y) // 関数の戻り値が err 含めて三つの場合
truthy
な値は true
だけであり、非ゼロの数値や空でない文字列を true
として扱うことはできない
x := 1
if x { // xがtruthyな値なら〜、のような条件式を書くことは出来ないため誤り
// ...
}
Goの定数はリテラルに名前を付与するものであり、変数がイミュータブルであることを宣言する方法はない
const x = 100 // このように定数宣言できるが、イミュータブルな変数を宣言しているわけではない
null安全について
null安全とは、実行時に null
が原因のエラーを発生させないような仕組みのこと
Goには完全なnull安全の仕組みはないが最低限の仕様は備わっている
宣言のみの(値を明示的に割り当てていない)変数にはその型によるデフォルトのゼロ値が入る
主なゼロ値として、bool は false
。int は 0
。string は 空文字(””
)。そして、スライスやインターフェース等は nil
関数内でも、変数をゼロ値に初期化する場合は var
を使用した宣言がイディオム的
var x int // この場合、int型のゼロ値である 0 が代入される
Goに NULL
はなく、代わりに nil
を使う。これは 0
の別名ではなく、値がないことを示す型のない識別子である
Goのランタイムが次の行動を見失った時に panic
が発生する
たとえば、ポインタで nil
の参照先を見に行くとpanicになる(nil
に参照先がないため)
var x []int // nilスライス
fmt.Println(x[0]) // ここで panic が発生する
panic: runtime error: index out of range [0] with length 0
panic
が起こると実行中の関数は即座に終了する
合成型について
スライス
- 順番に並んでいるデータを扱う
var xSlice = []int{1, 2, 3}
- 配列もあるが、宣言した後にサイズ(配列の長さ)を変えられない、など多くの制限がありGoではあまり使われない
- スライスでは、サイズの他にキャパシティ(メモリ内に確保した連続した領域)を設定できる
ySlice := make([]int, 3) // サイズ3, キャパシティ3のintスライス。値は、[0, 0, 0]
zSlice := make([]int, 3, 10) // サイズ3, キャパシティ10のintスライス。値は、[0, 0, 0]
マップ
- キーと値の間の対応関係を表現する。値の型は合成型も使用できる
- 要素を追加する際、キーも値も、(全て)宣言した型と同じ型でなければならない
sweetPrices := map[string]int{
"シュークリーム": 300,
"フルーツタルト": 450,
"カヌレ": 220,
}
- スライスでも、サイズの他にキャパシティを設定できる。スライス同様
make
を使用する - テーブルドリブンテスト(大量のテストケースを簡潔に表現できるテスト手法)でよく使う
構造体
- 関連するデータをまとめ、新たな型を定義する
- 各フィールドで異なる型を持つことが出来る
type Person struct {
name string
age int
birthplace string
}
共通
- 比較は
==
では(基本的には)できないため、reflect.DeepEqual
を用いる
構造の可変性 | 値の一意性 | 順序を持つか | |
---|---|---|---|
スライス | ○ | ✖️ | ○ |
マップ | ○ | ✖️ | ✖️ |
構造体 | ✖️ | ✖️ | ○ |
制御構造について
条件文
- if文とswitch文がある
rand.Seed(time.Now().Unix())
// if文の例。変数の宣言は省略可能
if x := rand.Intn(10); x == 0 { // xには0以上10未満のランダムな値が代入される
// ...
} else if x > 5 {
// ...
} else {
// ...
}
- switch文は、該当の
case
で処理を終えた後フォールスルーしない(条件文を抜ける) - if文と同様に、switch文でもブロック内だけで有効な変数を宣言可能。また、その値を使用し各
case
で論理演算子を使った比較も可能
teas := []string{"ジャスミン", "ルイボス", "アールグレイ", "チャイ"}
rand.Seed(time.Now().Unix())
// switch文の例。変数の宣言は省略可能
switch randomTea := teas[rand.Intn(len(teas))]; randomTea {
case "ジャスミン":
// ...
case "ルイボス", "アールグレイ":
// ...
case "チャイ":
// ...
default: // どのケースにも当てはまらない場合の処理
// ...
}
- 三項演算子はない
ループ
- ループはfor(とfor-range)だけ
-
for
に続いて、初期設定、条件、再設定の順に指定する。条件のみ または 全て省略(無限ループ)も可能 - ループ内部では
if
とcontinue
を組み合わせてネストを避けるのがイディオム的
// for文の例
for i := 0; i < 100; i++ {
if i % 10 == 0 {
// ...
continue
}
if i % 5 == 0 {
// ...
continue
}
if i % 2 == 0 {
// ...
continue
}
// ...
}
// for-range文の例
numbers := []int{1, 2, 3, 4, 5}
for i, v := range numbers { // ループ変数は、i(index)とv(value)の二つ
// ...
}
関数について
Goの関数は第1級関数である。これは、関数が変数と同じように扱える性質を指す
関数の引数には型が必須である
引数の後ろの )
と {
の間に戻り値の型を書く、なければ戻り値がないことを示す
func main() {
result := sum(1, 2)
fmt.Println(result) // 3
}
func sum(num1, num2 int) int { // 引数の型が同じ場合、最後の引数以外の型は省略できる
return num1 + num2
}
戻り値は複数あることも多いが、全て無視してもエラーにはならない
戻り値を変数に代入する際、使わない戻り値は _
で明示する
// for-range文の例(再掲)
numbers := []int{1, 2, 3, 4, 5}
for _, v := range numbers { // indexが必要ない場合、「_」で明示する
// ...
}
for i := range numbers { // 反対に、value(後ろの値)が必要ない場合は省略可能
// ...
}
戻り値のない関数の場合、途中で関数を抜ける場合を除き、関数の最後に ブランクreturn
は不要である(return
を明示的に書かなくてよい)
goは値渡しの言語と言われることがある。スライスやマップを除き、関数に引数を渡した際には必ず値のコピーが作成されている。そのため、呼び出し元の関数で渡した値が書き換わることはない
エラー処理について
Goに例外処理はない。代わりに関数からerror型を戻すことによってエラーを処理する
エラー処理の基本
- 関数が正常に処理された場合は、
err
にはnil
が返される - 異常終了した場合は、エラーを表す値が返される。また、それ以外の戻り値にはゼロ値を設定する
- 関数の呼び出し側で
err
をnil
と比較することでエラーの有無を判断する。エラーがあった場合は(直後に)エラー処理を行う
// 先ほどの「関数について」の例にエラー処理を反映し、一部拡張したスクリプト
func main() {
result, err := sum(0, 10)
if err != nil {
fmt.Println(err) // 引数には1以上の数を設定してください
os.Exit(1)
}
fmt.Println("合計: ", result)
}
func sum(num1, num2 int) (int, error) {
if num1 * num2 == 0 {
return 0, errors.New("引数には1以上の数を設定してください")
}
return num1 + num2, nil
}
errors.New
や fmt.Errorf
で文字列からエラーを生成することができる
fmt.Errorf("エラーが発生しました: %v", x) // fmt.Printfの全てのフォーマット指定子を使用可能
Goは独自のエラーを定義することができる。独自のエラーを定義し、使用することで「文字列の比較を避けられること」と「エラー処理のための付加情報を型に含めることができる」というメリットがある
独自エラーを適切に使用することにより、アプリケーションの規模が大きくなった場合でも型による早急な原因特定や情報の取得が可能となる
// これは、言語に標準組み込みのインターフェースのため、下のスクリプトと分けて記載しています
// 「独自エラーってこんな感じ」の例なので、一旦ここ読み飛ばしていただいて大丈夫です!
// 「インターフェースとメソッドについて」で補足説明します
type error interface {
Error() string
}
// 独自エラーを定義した構造体
type MyError struct {
When time.Time
}
// 構造体 MyError の新しいインスタンスを返す
func NewMyError() error {
return &MyError{
When: time.Now(),
}
}
// こちらについても、一旦読み飛ばしていただいて大丈夫です!
func (e *MyError) Error() string {
return fmt.Sprintf("Occurred at %s", e.When.Format(time.RFC3339))
}
func main() {
err := doSomething()
if err != nil {
var myErr *MyError
// エラーの型が MyError かどうかを確認
if errors.As(err, &myErr) {
fmt.Printf("MyErrorが発生しました: %v", myErr) // (出力例)MyErrorが発生しました: Occurred at 2023-12-10T12:34:56Z
} else {
fmt.Println("未知のエラーが発生しました")
}
os.Exit(1)
}
fmt.Println("処理は正常に完了しました")
}
func doSomething() error {
// 何かしらのエラーが発生したと仮定
return NewMyError()
}
fmt.Errorf
を使用しエラーのフォーマット文字列に %w
を含めることで、エラー情報をラップすることができる。これにより元の情報を維持したまま別の情報を付加することができる
func main() {
err := doSomething()
if err != nil {
// %w を使用し、エラー情報をラップ
wrappedErr := fmt.Errorf("wrapped error: %w", err)
fmt.Printf("エラーが発生しました: %v", wrappedErr) // エラーが発生しました: wrapped error: (doSomethingから返されたエラー内容)
// ...
}
// ...
}
エラーが発生した場合でも(もちろん正常に処理が完了した場合でも)、使用したリソースを確実に解放しておきたい
後で確実に実行しておきたい処理の前に defer
を置くことで(return
の後に)必ず実行してくれる
func main() {
// データベースへの接続
db, err := sql.Open("sqlite3", "example.db")
if err != nil {
// エラーが発生した場合、ログメッセージを出力してプログラムを即座に終了する
log.Fatalf("データベース接続に失敗しました: %v", err)
}
defer func() {
if err := db.Close(); err != nil {
log.Printf("データベースのクローズに失敗しました: %v", err)
return
}
fmt.Println("データベースが正常にクローズされました")
}()
// ...
}
「null安全について」で、Goのランタイムが次の行動を見失った時にpanic
が発生すると述べたが、これはエラー処理で捕捉できなかった例外が発生したことを指す
そのような場合でも、recover()
をdefer
と組み合わせて使用することにより強制終了を回避することができる
具体的には、panic
の内容を確認し、発生場所の次の行から処理を再開できる場合がある
モジュールとパッケージについて
-
モジュールはアプリケーションのルートである。原則としてリポジトリに一つ
-
中央集権的なリポジトリには依存せず、各プロジェクトは自身の
go.mod
ファイルに依存関係を宣言し独立している -
一つ以上のパッケージからモジュールが構成される
-
パッケージには、どのような機能を提供するのかわかる命名をし、無理に少ないパッケージで全てを管理しようとせず機能ごとにパッケージを分けるのがよいとされる
現在のファイルがどのパッケージに属するかをファイルの最初の行に定義する
package profile
import (
// ...
)
func helper() {
// ...
}
別のファイルに宣言した関数でも同じパッケージ内であればそのまま使える
// 以下のような場合、helper.goで宣言した関数を他のファイルでimportせずに使用できる
profile/
├── get.go
├── edit.go
└── helper.go
パッケージ名とそのファイルを含むディレクトリ名は同じにすべきである
サードパーティのパッケージをインポートする際にはソースコードリポジトリの場所を指定する
ファイルに同じモジュール内の別パッケージをインポートする場合でも、絶対パスを使用する方が保守性がよい
また、Goにはパッケージブロック(関数の外側)の他にユニバースブロックという場所がある
組み込みの型や定数、nil
などをユニバースブロックで定義し、キーワードではなく、あらかじめ宣言された識別子として扱っている
たとえば関数の中で nil
という変数を宣言し、使うことも出来てしまうので注意する(宣言語その関数では nil
は変数として扱われる)
ポインタについて
-
ポインタとは、値が保存されているメモリアドレスを指す変数
-
スライス、マップ、関数、インターフェースはポインタで実装されている
-
ポインタのゼロ値は
nil
であるため、上記の型のゼロ値もnil
となる
&
は、変数の前につけるとその変数のアドレスを返すアドレス演算子
*
は、二つの異なる文脈で使用される
- 変数を宣言する際に型の前に使用すると、ポインタ型の宣言となる
- ポインタ型の変数の前につけると、そのポインタが参照する値を返すデリファレンス用の演算子
x := 10
pointerX := &x
fmt.Println(pointerX) // アドレスが表示される
fmt.Println(*pointerX) // 10
y := 20 + *pointerX
fmt.Println(y) // 30
var z *int // ポインタ型として宣言
fmt.Println(z == nil) // true
ポインタを使うとデータの流れがわかりにくくなるため、使用はできるだけ慎重にするべき
たとえば、構造体を返す関数を作成する際は、関数に構造体のポインタを渡して中身を埋めてもらうのではなく、関数の中で構造体のインスタンスを作成する
// Person 構造体を定義
type Person struct {
name string
age int
birthplace string
}
// 構造体 Person の新しいインスタンスを作成して返す
func newPerson(name string, age int, birthplace string) Person {
return Person{
name: name,
age: age,
birthplace: birthplace,
}
}
func main() {
john := newPerson("John", 25, "New Jersey")
fmt.Printf("Name: %v, Age: %v, Birthplace: %v", john.name, john.age, john.birthplace)
}
インターフェースとメソッドについて
ユーザー定義の型に付随する関数を定義することができ、これをメソッドと呼ぶ
メソッドを定義する際、キーワード func
とメソッド名の間の レシーバ に型を記載する
メソッドのレシーバには基本的にポインタ型を使用する(参照が必要な処理が多いため。逆にそれが不要ならメソッドではなく関数を使用すればよいことが多い)
// 先ほどの「ポインタについて」の例にメソッドを定義し、一部拡張したスクリプト
// Person 構造体を定義
type Person struct {
name string
age int
birthplace string
}
// 構造体 Person の新しいインスタンスを作成して返す
func newPerson(name string, age int, birthplace string) Person {
return Person{
name: name,
age: age,
birthplace: birthplace,
}
}
// Person型に付随するStringメソッドを定義
func (p Person) CreateProfile() string { // (p Person)でレシーバにPerson型を指定している
return fmt.Sprintf("Name: %v, Age: %v, Birthplace: %v", p.name, p.age, p.birthplace)
}
func main()
john := newPerson("John", 25, "New Jersey")
profile := john.CreateProfile()
fmt.Println(profile) // Name: John, Age: 25, Birthplace: New Jersey
}
同じ処理内容のメソッドを複数の型が使い回すことができる
Goは「クラス継承より、オブジェクト合成の方がよい」という考え方をさまざまなところで取り入れている
合成とは、親子関係のない複数の異なる要素や機能を組み合わせて新しい機能を作り出す方法
Goに「継承」は存在せず、そのように見えるものは「埋め込み」であることが多い
埋め込みとは、他の型の機能を再利用するための手段であり、そこに依存関係はない
型には具象型(データの記憶のされ方を規定する型)と抽象型(型が何をするものなのかだけ規定し、実装を提供しない型)がある
インターフェースはGoで唯一の抽象型であり、特定の具象型が満たすべき要件(メソッドの集まり)を示す
インターフェースと構造体を組み合わせ、クラスのような振る舞いを定義できる。これは、既存のコードを変更せずに機能を拡張できる点で大きなメリットがある
古い記事だが「グーグル、C/C++に代わる新言語「Go」をOSSで公開」では、インターフェイスについて、おそらくGoの中でもっとも斬新なアイデアだと表現されている
「エラー処理について」の独自エラー型のスクリプトで一部説明を省略した部分について、改めてコメントを追加しました。
error
は言語に組み込みのインターフェースです。Error
メソッドを一つだけ持っており、これは単にエラーを表す文字列を返すシンプルなメソッドです。
// errorは標準組み込みのインターフェース
type error interface {
Error() string
}
// 独自エラーを定義した構造体
type MyError struct {
When time.Time
}
// 構造体 MyError の新しいインスタンスを返す
func NewMyError() error {
return &MyError{
When: time.Now(),
}
}
// MyError型に errorインターフェースを実装している
// 処理内容としては、エラー発生時刻を含む詳細な説明を文字列として返している
func (e *MyError) Error() string {
return fmt.Sprintf("Occurred at %s", e.When.Format(time.RFC3339))
}
func main() {
err := doSomething()
if err != nil {
var myErr *MyError
// エラーの型が MyError かどうかを確認
if errors.As(err, &myErr) {
// fmtパッケージの関数がerrorインターフェースを実装した型の変数(ここではmyErrのこと)を扱う際に、自動的にErrorメソッドが呼び出される
// そのため、最終的にエラー発生時刻を含んだエラーメッセージが出力される
// (出力例)MyErrorが発生しました: Occurred at 2023-12-10T12:34:56Z
fmt.Printf("MyErrorが発生しました: %v", myErr)
} else {
fmt.Println("未知のエラーが発生しました")
}
os.Exit(1)
}
fmt.Println("処理は正常に完了しました")
}
func doSomething() error {
// 何かしらのエラーが発生したと仮定
return NewMyError()
}
テストについて
お恥ずかしながら私は学生時代に全くテストを書いたことがありませんでした、、
TDD(テスト駆動開発)……ほぅ、、なんか難しそうだなー? うん、まぁいいか。とか思ってました、、
そのため研修を終えて実業務に移ってから、Goで1番理解に時間がかかったこと・学んだことがテストだと言えるかもしれません。
テストコードを書かなくても形にしたいアイデアを実現することはできますし、他の言語と比較して「学び始めに理解を難しくさせていたこと」という定義には合っていないかも、と思います。
しかしGoの性質を考えればテストは切っても切り離せないため、ここではテストの重要性だけ触れさせてください。
テストの最大の目的は「コード品質を保つ」こと、これに尽きます。
当たり前かもしれません。しかし、限られた時間でさまざまなやるべきことがある中で、重要項目の一つとして優先度高く対応できていないことも多いのではないかと思います。
テストが充実していると、具体的には以下のようなメリットがあります。
- コードの振る舞いや期待される結果が明示的になる
- 思わぬデグレの早期発見ができる
- チームで仕様の共通理解ができる
Goでは、標準ライブラリのtesting
パッケージでテスト作成を支援する型と関数を定義しています。
また、HTTPサービスを呼び出す関数のテストを簡単にできるnet/http
パッケージがあります。
そして、go test
を叩き、作成したテストに対して実行とレポート作成を行うことができます。
(他の言語でテストコードを書いたことがある場合は、)テストを比較的簡単にできるよう支援されていますので、ある程度慣れてきたら次に学ぶこととしておすすめしたいです!!
他に学んでおくとよさそうなこと
その他、学んでおくとさまざまな場面で役に立ちそうなことを並べてみました。
これらは学び始めに詰め込みすぎる必要はあまりないかなと感じていますが、必要な場面で少しづつ学んでいくことをおすすめしたいです。
(重要度が低いからではなく、ここまで紹介させていただいたことの方が優先度が高いと考えるためです。)
- 標準ライブラリ
- database/sqlパッケージ、encoding/jsonパッケージ等色々あるので知ってること増えると便利そう
- 標準ライブラリがまとまっている場所はこちら
- コンテキスト
- Goにおける、リクエストのメタデータを持ち運ぶための仕組み(contextパッケージ)
- ログ
- 従来の
log
パッケージはできることが少なく、実質サードパーティのライブラリを利用する必要があった。その問題を解決するために最新のGo1.21からlog/slogパッケージが追加された
- 従来の
- ゴルーチン
- Goは現在使われている主要な言語の中でもかなり実行速度が速いという側面がある
- ゴルーチンと呼ばれる軽量なスレッドを利用し、効果的にマルチスレッド処理を実現しているため、複数のCPUコアを最大限利用できることが要因の一つである
おわりに
アドベントカレンダー楽しそうだなー! 学んだことをまとめるのにいいきっかけかもなーと思い、初めてQiita記事書いてみましたが、「あ、説明できないこれ、、」がいっぱい見つかり、やってみてよかったと思いました!
今回、各項目についてさまざまな場面でどう使うかだったり、実務を想定したサンプルコードもあまり入れていなかったので、次は何か具体的な記事を書いてみたいと思いますー!
最後に、これからGoを学び始める方に一つだけ、スムーズに環境構築が進められるようにGoのディレクトリ構成について触れて締めたいと思います!
- コードの構成方法は何度か変わってきた歴史がある
- 現在では
GOROOT
もGOPATH
も設定しなくても問題ない - 自分が開発するプロジェクトは任意の場所に置くことができる
- サードパーティのモジュールやパッケージはホームディレクトリの下にダウンロードされる
- コマンドは
go/bin
ディレクトリの下にインストールされるので、このディレクトリをコマンドの検索パスに入れる必要があるかも
参考記事, 書籍, 教材
- グーグル、C/C++に代わる新言語「Go」をOSSで公開
- Go 1 と Go プログラムの将来
- Jon Bodner 著「初めてのGo言語」(オライリー・ジャパン発行)
- A Tour of Go
- Goチュートリアルを公開しました!