はじめに
本記事はmediba Advent Calendar 2023の18日目の記事です。
私は、今年の9月に株式会社medibaにバックエンドエンジニアとして入社しました。
弊社ではバックエンドにGoを採用することが多く、Goに入門した際にハマったこと(調べたこと)を書いていきます。
同じくGoの学習する方の少しでも助けになれば幸いです。
配列とスライス
Goでは複合データ型を表現するものとして、配列とスライスがあります。
配列は固定長でappend()
で拡張不可です。
// 配列を作成
ary := [3]int{1,2,3}
// 拡張不可
ary = append(ary, 4)
// -> error! first argument to append must be a slice
一方、スライスは可変長であり、append()
で拡張可能となっております。
// スライスを作成
slc := []int{4,5,6}
// 拡張可能
slc = append(slc, 7)
配列には固定&拡張不可という制限があるため、実際はスライスを使うことが多そうです。
関数の引数は値渡し
Goでは、関数に引数を渡した際、基本的にはその値のコピーが渡されます(=値渡し)
そのため、関数の中で引数を上書きする処理をして、その関数を実行した後でも、上書きしようとした値は変更されません。
func main() {
i := 1
fmt.Println(i)
// -> 1
// 渡された引数に1を足す関数を実行
plusOne(i)
fmt.Println(i)
// -> 1
// 値渡しのため、iは2にはならず1のままである
}
func plusOne(i int) int {
return i + 1
}
ただし、スライスとマップを引数として値を渡した場合は、内部的にはポインタを渡しているため、値が変更されます。
つまり、ポインタを渡すことによって、その値を変更することができます。
ただしのただし、スライスの値の変更は反映されるが、append()
を使用した値の追加は変更されません。
参考:Try Golang! Sliceってポインタなの?それともポインタじゃないの?
func main() {
m := map[string]string{}
s1 := []string{"abc"}
s2 := []string{"ABC"}
addMap(m)
addSlice(s1)
changeSlice(s2)
fmt.Printf("m: %v, s1: %v, s2: %v\n", m, s1, s2)
// -> m: map[a:abc], s1: [abc], s2: [XYZ]
// s1は変更されていない
}
func addMap(m map[string]string) {
m["a"] = "abc"
}
func addSlice(s []string) {
s = append(s, "DEF")
}
func changeSlice(s []string) {
s[0] = "XYZ"
}
上記の通り、スライスへの要素追加は定義元には反映されていないことが確認できます。
ポインタ
ポインタをわかりやすく説明してくれている記事は沢山あるので、簡単に特徴をまとめます。
- ある値が保存されているメモリ内の位置(アドレス)を表す変数
- どのような型を参照していてもサイズが同じ
- どんなに小さいor大きい値だとしても、アドレス自体のサイズは変わらないため
- アドレス演算子
&
を変数の前につけると、その変数のアドレスを返す - 関節参照の演算子
*
をポインタ型変数の前につけると、そのポインタが参照するアドレスに保存されている実際の値を返す - ポインタはミュータブル(変更可能)の印である
- 上の「値渡し」で触れた通り、ポインタを渡すことによって関数内でその値を変更することができる
ポインタは中々馴染みがない概念で、使用する度に調べている気がしますw
完全に理解した状態になれたら、改めてアウトプットしようと思います。
モジュールとパッケージ
何となくその時の雰囲気で使いがちなこの2つの言葉ですが、違いをきちんと理解しておいた方が情報収集が楽になりそうだったのでまとめたいと思います。
モジュール
- Goアプリケーションのrootになり、パッケージの集合体
- GithubのようなVCSで管理するリポジトリに保存される
-
go.mod
が存在する(go.mod
については後述します)
パッケージ
- ソースコードの集合体(フォルダ)
- モジュールに1つもしくは複数含まれるもの
- 各goファイルの1行目で
package パッケージ名
と宣言する
大小関係をツリー構造で表すと以下のような形になります。
モジュールとパッケージの関係性が、感覚とは逆でした。
└── リポジトリ
├── モジュール
│ ├── パッケージ1
│ └── パッケージ2
参考:Modules, packages, and versions
モジュール管理
Goでモジュール管理をする際に登場するファイルやコマンド群の紹介です。
go.mod
Goにおいて、モジュールを扱いたい場合は依存関係を管理するためのgo.mod
ファイルが必要です。
go mod init {module_path}
というモジュールを初期化するコマンドを実行することでgo.mod
を生成することが可能です。
go.mod
には、自身のmodule名や使用する Goのバージョン、使用しているモジュールとそのバージョンなどが管理されます。
下記のgo mod tidy
を実行することで、go.mod
の内容は自動で更新されます。
go mod tidy
go mod tidy
は、モジュール操作をよしなにやってくれるコマンドです。
具体的には、ソースファイルを解析し、モジュールのダウンロードや削除を行います。
ちなみにtidyはティディではなく、タイディと読むらしく、「整然とした」「綺麗好きな」のような意味らしいです。
依存関係を整理(追加や削除等)してくれるものだということが名前から予想できます。
サードパーティのパッケージをインポートしたい時、まずgoファイルの上部にimport句を書きます。
以下はrsc.io/quoteという格言を集めたパッケージを使用する例です。
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Go())
}
この状態では、まだインポートできておらずコンパイルできません。
go mod tidy
を実行することで、モジュールに関する必要な操作をよしなにやってくれます。
$ go mod tidy
go: finding module for package rsc.io/quote
go: downloading rsc.io/quote v1.5.2
go: found rsc.io/quote in rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
エラーが消え、コンパイルできるようになりました。
$ go run main.go
# -> Don't communicate by sharing memory, share memory by communicating.
go.sum
go mod tidy
を実行すると、go.sum
ファイルも作成(更新)されます。
go.sum
の中身には以下が含まれております。
- モジュールとそのバージョン
- モジュールのチェックサム(ハッシュ)
- そのモジュールの
go.mod
ファイルのチェックサム
Googleが管理しているチェックサムデータベースというものがあり、それと比較することでモジュールの改ざんを防止してくれるようです。
(余談)モジュールモードまでの変遷
上で説明したものはモジュールモードというものになりますが、実はGo1.10以前はモジュール機能はなく、GOPATHモードと言われるもので動作していたようです。
GOPATHモードは様々なデメリットがあり、1.13にモジュール機能が追加、1.17以降はモジュールモードがデフォルトになっております
(この辺りは詳しく解説されている記事が沢山あるので、詳細は割愛します)
参考:Go言語のパッケージ管理方法(GOPATHモードとmodule-awareモード)について理解を深める
古い記事では、GOPATHモードでの解説がちらほらあったりするので、注意が必要です。
最後に
現在medibaでは、メンバーを大募集しています。
募集・応募ページ
medibaってどんな会社だろう?と興味を持っていただいた方は、カジュアル面談もやっておりますので、お気軽にお申込み頂ければと思います。