Go言語の基礎文法
はじめに
この記事は、Go言語の基礎文法をサクッと把握するために書きました。
私自身は、PHPやNode.jsなどLL言語をよく書いており、それらと比較しながら把握しやすいようにまとめました。
内容は少し端折っていますが、他の言語経験者であれば楽に読み進められると思います。不明な点などは詳細な解説記事を参照してもらえればと思います。
サンプルコードをgithubにアップしましたので、良ければ動かしながら確認ください。
Go言語とは
Googleが開発した言語で、2009年にOSSとして公開、2012年にバージョン1.0が公開されました。
2023年2月時点での最新バージョンは1.20となっています。
特徴としては以下のような点があります。
- 構文がシンプル
- 型があり曖昧な記述ができない
- goroutineで並列処理が簡単に実装できる
- 標準パッケージ、周辺ツールが豊富にある
- シングルバイナリで実行できる
- クロスコンパイルが可能
コンテナ運用と相性が良いと言われており、
理由としては、シングルバイナリでコンテナサイズを小さく保ちやすいなどが挙げられていますが、C言語やRustなどでも同様で、明確な理由は若干不明です。
パッケージ
Goのプログラムは何らかのパッケージに属しており、package パッケージ名
で宣言します。
他のプログラムを利用する場合は、import
でパッケージを読み込みます。
Goのプログラムはmainパッケージのmain関数から実行されるというルールがありますので、コードリーディングするときはmain関数をまず探すと読み進めやすいです。
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World.")
}
変数
変数の宣言方法は、以下3つの方法があります。
型を指定しない場合は、代入された値により型推論されます。
利用頻度としては、③ > ② で、①はあまり利用しません。
// ① 型を指定
var name string = "yamada"
// ② 型を省略
var name = "yamada"
// ③ varを省略(関数の中でのみ利用可能)
name := "yamada"
変数をまとめて宣言することもできます。
②の方法はよく利用します。
// ① 同じ型の変数をまとめて定義
var i, j, z int
i, j, z = 1, 2, 3
// ② 異なる型の変数をまとめて定義
var (
name = "yamada"
age = 30
)
データ型
Go言語が最初から用意しているデータ型は以下の種類があります。
基本的には以下の型を使用し、必要に応じて独自定義の型を利用していきます。
分類 | 型 |
---|---|
整数 | int, int8, int16, int32, int64 uint, uint8, uint16, uint32, uint64 uintptr, byte, rune |
浮動小数点 | float32, float64 |
文字列 | string |
真偽値 | bool |
変数宣言時に値を指定しない場合は、ゼロ値が設定されます。ゼロ値は方によって定義されています。
※ 動的言語とは大きく異なる点です
var count int // 「0」が入る
var text string // 「""」空文字になる
var flg bool // 「false」になる
定数
値が変わらないものを定数として定義して利用することができます。
定数は再代入ができないようになっており、型を明示しない場合は変数と同様に型推論されます。
定数の定義には、const
を使います。
// 定数の定義
const name string = "yamada"
const name = "yamada"
// まとめて定義(型を指定しない場合は型推論)
const (
name string = "yamada"
age = 30
)
関数
関数は、一連の処理をまとめたもので、func 関数名(引数の定義) 戻り値の型 { 処理 }
で定義します。
// 基本の形
func sum(x, y int) int {
return x + y
}
// 複数の戻り値
func hello(x, y int) (int, int) {
return x, y
}
x, y := hello(1, 2) // カンマ区切りで受け取る
Go言語では無名関数を利用することもできます。
main() {
name := "yamada"
// 無名関数(関数定義と同時に実行)
func() {
fmt.Println(name) // "yamada"と出力される
}()
// 関数を変数に代入して実行する
print := func() {
fmt.Println(name)
}
print()
}
演算子
算術演算子。
整数で割り算を行うと結果が整数になる点に注意。
結果を少数で得たい場合は浮動小数点を指定する必要があります。
// 加算
res := 1 + 2 // 3
// 減算
res := 2 - 1 // 1
// 掛け算
res := 3 * 2 // 6
// 割り算
res := 4 / 2 // 2
res := 5 / 2 // 2
res := 5.0 / 2 // 2.5
// 余り
res := 8 / 3 // 2
代入演算子。
i := 1
i += 2 // 3
i -= 1 // 2
i++ // 3
i-- // 2
if文
if 条件 { 処理 }
で指定します。
条件部分はbool型を返すものであれば良く、関数も使えます。
name := "yamada"
if name == "yamada" {
fmt.Println("名前はyamadaです。")
}
i := 2
if i%2 == 0 {
fmt.Println("値は偶数です。")
} else {
fmt.Println("値は奇数です。")
}
if i != 0 && i != 1 {
fmt.Println("値は0でも1でもありません。")
}
if i == 0 || i == 2 {
fmt.Println("値は0または2です。")
}
// 「;」を使うと代入と式を1行でかける
if age := 20; age >= 20 {
fmt.Println("年齢は20歳以上です。")
}
for文
for 初期値; 継続条件; 処理
で繰り返しの処理を行います。
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// 式のみの指定も可能
j := 0
for j < 5 {
fmt.Println(j)
j++
}
// continue: 次のループへ, break: for分を抜ける
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
if i > 8 {
break
}
fmt.Println(i)
}
// rangeを利用した繰り返し処理
names := []string{"yamada", "sato", "suzuki"}
for index, name := range names {
fmt.Println(index, name)
}
switch文
switch 式 {}
で指定し、case 値: 処理
で式の結果と一致するcaseの処理が実行されます。
caseの処理が終わるとそのままswitchブロックを抜けます。次のcaseに移りたい場合は、fallthrough
を使う。
name := "yamada"
switch name {
case "sato":
fmt.Println("名前はsatoです")
case "yamada", "suzuki":
fmt.Println("名前はyamadaまたはsuzukiです")
default:
fmt.Println("名前はこれら以外です")
}
// case1, 2まで処理される
i := 1
switch i {
case 1:
fmt.Println("値は1です")
fallthrough
case 2:
fmt.Println("値は2です")
case 3:
fmt.Println("値は1です")
}
defer
defer f()
で関数の呼び出し元に戻る前に関数を実行します。
複数ある場合は、LIFOの順で処理されます。
ファイルの読み込み時に、忘れずに閉じるように使ったりする。
func main() {
defer fmt.Println("4番目の処理")
defer fmt.Println("3番目の処理")
defer fmt.Println("2番目の処理")
fmt.Println("1番目の処理")
}
配列
[要素数] 型 { 値, ... }
で定義し、同じデータ型を並べたデータ型。
要素数を変更することはできません。可変長で扱いたい場合はスライス型を使います。
// 基本の形
arr := [3]int{1, 2, 3}
fmt.Println(arr) // [1,2,3]
// 添字でアクセス
fmt.Println(arr[1]) // 2
// 要素数を省略
names := [...]string{"yamada", "sato", "suzuki"}
fmt.Println(names) // ["yamada", "sato", "suzuki"]
// 配列の長さを取得
fmt.Println(len(names)) // 3
スライス
配列の一部を切り出した構造体。配列[開始:終了]
で切り出します。
切り出したスライスは配列を参照しているため、値を変更すると配列の値も変更されます。
// 基本の定義方法
arr := [...]int{1, 2, 3, 4, 5}
arr1 := arr[2:4]
fmt.Println(arr1) // [3 4]
// スライスの変更が配列に反映される
arr1[0] = 6
fmt.Println(arr) // [1 2 6 4 5]
// 配列の変更がスライスに反映される
arr[3] = 7
fmt.Println(arr1) // [6 7]
また、スライスは可変長配列として利用することもできます。
(使い方としてはこちらの方が多い)
// 初期化
i := []int{0,1,2}
// 要素を追加①
i = append(i, 3, 4)
fmt.Println(i) // [0 1 2 3 4]
// 要素を追加②
j := []int{5, 6}
マップ
map[キーの型]値の型
で定義し、キーと値をマッピングさせたデータ構造。
// 初期化
value := map[string]int{"a": 1, "b": 2}
fmt.Println(value) // map[a:1 b:2]
// キーを指定して取得
fmt.Println(value["a"]) // 1
// 2つ目に存在するかのbool値が返ってくる
val, exist := value["b"]
fmt.Println(val, exist) // 2, true
// 存在確認
if _, exist := value["c"]; exist {
fmt.Println("cの要素は存在します。")
} else {
fmt.Println("cの要素は存在しません。")
}
// 削除
delete(value, "b")
fmt.Println(value) // map[a:1]
構造体
データ型が異なる変数を集めたデータ型です。
// 初期化
yamada := struct {
Name string
Age int
}{
Name: "yamada",
Age: 20,
}
fmt.Printf("%+v", yamada) // {Name:yamada Age:20}
// フィールドにアクセス
yamada.Age = 30
fmt.Println(yamada.Age) // 30
type
を使うことで構造体の型を定義できます。
type Human struct {
Name string
Age int
}
func main() {
// 初期化
sato := Human{
Name: "sato",
Age: 20,
}
fmt.Printf("%+v", sato) // {Name:sato Age:20}
}
メソッド
メソッドは、型に関数を紐づけたものです。
メソッドを使うことで型に関数を定義することができます。
(オブジェクト指向のインスタンス変数のようなもの)
type Human struct {
Name string
Age int
}
// ① メソッド定義
func (h Human) SayName() {
fmt.Println(h.Name)
}
// ② レシーバーは受け取らなくても良い
func (Human) Hello() {
fmt.Println("hello")
}
// ③ 他のメソッドを呼び出すこともできる
func (h Human) Say() {
h.Hello()
h.SayName()
}
// ④ 元データの変更(ポインタを使用)
func (h *Human) SetName(name string) {
h.Name = name
}
func main() {
yamada := Human{
Name: "yamada",
Age: 20,
}
yamada.SayName()
yamada.Hello()
yamada.Say()
yamada.SetName("sato")
yamada.SayName()
}
ポインタ
ポインタはメモリのアドレスを扱う変数で、ポインタを利用する場合はポインタ型で定義します。
図のように、変数はアドレス(メモリ上の住所)に紐づいており、アドレスが値に紐づいています。
name2
に name1
のアドレスを設定すると、同じ値を指すことになります。
また、アドレスに紐づく値を変更すれば、name1
・name2
のどちらの値も変わることになります。
func set(i *int) {
*i = 5
}
func main() {
// 型を定義
var i int
var ip *int // ゼロ値はnill
fmt.Println(i, ip)
// アドレスを代入
// ip = i // 型が違うのでエラーになる
ip = &i // 変数iのアドレスをipに代入
fmt.Println(ip)
// 値の代入や取得には*をつける
*ip = 1
fmt.Println(*ip)
set(&i)
fmt.Println(i) // 5になる
}
使いどころとしては、関数内で引数を変更する場合などで利用する。
構造体のメソッドでよく利用される。
type Human struct {
Age int
}
func (h *Human) addAge() {
h.Age += 1
}
func main() {
// ポインタの利用例
human := Human{Age: 10}
human.addAge()
fmt.Println(human.Age)
}
インターフェース
インターフェースは関数の集まりで、型がどのようなメソッドを持つか(どのように振る舞うか)を定義した型です。
インターフェースで定義した関数を持っていない場合はエラーになります。
他の言語の場合はクラス定義時にimplements
することが多いですが、Goの場合は初期化時に指定します。
package main
import "fmt"
type Human interface {
SayName()
}
type Man struct {
Name string
}
func (m Man) SayName() {
fmt.Println(m.Name)
}
type Woman struct {
Name string
}
func main() {
var man Human = Man{Name: "yamada"}
man.SayName()
// SayNameメソッドを実装していないためエラーになる
// var woman Human = Woman{Name: "yamada"}
}
インターフェースの詳しい使い方や利用シーンについては下記の記事が参考になります。
エラー
Goには例外処理がなく、エラー型を使ってエラーの処理を行います。
error
はError() string
メソッドを定義したインターフェースで、Error
メソッドを実装すれば自分でエラーを定義できます。
f, err := os.Open("sample.txt")
if err != nil {
fmt.Println(err)
}
defer f.Close()
自前のエラーを作成する方法は以下2つの方法があります。
// ① Error() string を実装する
type MyErr struct {}
func (MyErr) Error() string {
return "my error"
}
// ② 標準パッケージを使う
var myErr = errors.New("my error")
並列処理
Goでは、go
ステートメントを使うことで並列処理を簡単に実装することができます。
// 複数の並列処理の順序は保証されない
go func() {
fmt.Println("① 出力2-1")
}()
go func() {
fmt.Println("① 出力2-2")
}()
fmt.Println("① 出力1")
time.Sleep(1 * time.Second)
fmt.Println("① 出力3")
並列処理の完了を待つ場合は、sync.WaitGroup
を使います。
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("② 出力1-1")
}()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("② 出力1-2")
}()
wg.Wait()
fmt.Println("② 出力3")
実際にはもっと複雑な処理を書くことになりますので、こちらの記事が参考になります。
最後に
今回は私がGo言語を始めるのにあたり基礎的な文法を抑えるために、LL言語と比較しつつまとめました。
感想・コメントなどあれば、是非いただければ幸いです。
Discussion
シンタックスハイライトを付けてくださるともっと見やすくなると思います
ありがとうございます!修正しました🙇