プログラミング
記事内に商品プロモーションを含む場合があります

Go言語プログラミング入門|基本文法・構文総まとめ

Aru

Go言語に興味を持った人に向けて、プログラミングに必要となる文法や基本的な概念を網羅した記事を作成しました。他のプログラミング言語を学習したことがある方であれば、この記事を読むことでGo言語の全体像を理解できると思います。この記事を通じて、Go言語の基本を理解できます。

はじめに

Go言語は「Go言語」や「Golang」と表記されるGoogleが開発したプログラミング言語です。この記事は、Go言語の基本知識を一通り学べるように構成しています。

Go言語に興味を持った、既に他のプログラミグ言語を使っている方が、Go言語の文法や使い方を理解することができる内容を目指しています。

なお、Go言語はパッケージ管理やテストコードなどの部分にも特徴があるのですがその手の内容は解説していません。そちらについては実際に開発に使い出してから徐々に理解すれば良いと考えています。

この記事を通じて、Go言語の全体像をつかみ、さらに興味を持っていただければ幸いです。

私は、Go言語の可読性の高さが結構気に入っています。他の言語と比較して圧倒的に「他人が書いたプログラムが読みやすい」です。これがGo言語の利点だと思っています。

あわせて読みたい
AtCoderで役立つ記事一覧(Python, Go言語)
AtCoderで役立つ記事一覧(Python, Go言語)

Hello World〜基本

まずはHello, world!

Go言語のプログラムの拡張子は.goです。以下は、定番のHello, world!を表示するプログラムになります。

直接実行する場合は、以下のようにします

go run main.go

また、コンパイルして実行する場合は、以下のようにします。

go build main.go
./main

コンパイルという言葉からわかるように、Go言語はコンパイル時に型が変数の方が決まる静的型付け言語です。このため、Pythonなどのインタプリタ言語と比較して高速です。

C++と比較した場合、行末尾の;(セミコロン)が必要ない点もポイントです。

package main

import "fmt"

// メイン関数
func main() {
    fmt.Println("Hello, world!")
}

Visual Studio CodeのGolangプラグインの機能が充実していますので、VSCodeを使ってプログラミングすることをお勧めします。importの部分の自動挿入など、かなりのことをやってくれます。

まずは、Hello, world!で使った要素を説明していきます。

パッケージ(package)

パッケージはコードをまとめる基本単位です。

プログラムでは必ずmainパッケージが存在する必要があり、mainパッケージ内にmain()関数が定義されている必要があります

Hello, World!では、このファイルがmainパッケージになるので、

package main

と先頭で定義しています。

ライブラリなどの場合は、package hogelibのようにパッケージ名をしています

インポート(import)

Go言語ではimport文によりパッケージをインポートできます。

Hello, world!では、fmt.Println()を利用するためにfmtパッケージをインポートしています。

インポートは以下のようにそれぞれのパッケージを記述することも可能です。

import "fmt"
import "os"

また、以下のように()で括ることで、まとめて記述することも可能です。

import (
    "fmt"
    "os"
)

関数(func)

Go言語では、関数の定義はfuncで行います。main()関数は、引数も戻り値もないため、

func main() {...}

という定義になります。プログラムは、このmain関数から実行されることになります。

他のプログラミング言語(C言語など)を知っていれば、ここは馴染みがあると思います。

fmt.Print, fmt.Println, fmt.Printf

パッケージ内の関数を利用する場合、パッケージ名.関数名という形で呼び出します。fmt.Printは、fmtパッケージのPrint関数を呼び出すという意味になります。

Print文には、Print()と最後に改行の入るPrintln()と、フォーマットが指定できるPrintf()があります。

Hello, world!では、fmt.Println("Hello, world!")と、”Hello, world!”を表示して改行を行なっています。

なお、Printfで利用できるフォーマットには以下のようなものがあります(ここに挙げたものは代表的なものだけです)

%v, %#v, %t(真理値), %d(整数), %s(文字列), %c(文字), %f(少数), %F(少数), %e(浮動小数点e表記), %E(浮動小数点e表記), %g(%f/%e自動選択), %b(2進数), %o(8進数), %x(16新数), %U(Unicode), %p(ポインタ), %T(型を表示)

例えば、以下のように利用します。

fmt.Printf("%d %s\n", num, str)

C++とは違い、Pythonなどと同じように、配列の内容を表示したり、構造体の内容を表示させたりすることができます。

コメント

Go言語のコメントは//で始まる行または、/**/で囲んだ範囲になります。

// コメント
/* コメント行
  コメント行 */

 予約済みキーワード(予約語)

Go言語の予約語は以下になります

break, case, chan, const, continue, default, defer, else, fallthrough, for, func, go, goto, if, import, interface, map, package, range, return, select, struct, switch, type, var

予約語とは、プログラミング言語の仕様に定められており、識別名として利用できない文字列のことです。

リテラル

リテラルとは、プログラムのソースコードにおいて使用される、数値や文字列を直接に記述した定数のことです。Go言語では、以下のようなものがリテラルになります

nil:値が存在しないことを示す特別な値
true/false:真偽値
1234:整数
O123:8進数
0o123:8進数
0x123:16進数
0b0101:2進数
1.234:少数
1.23e4:浮動小数点
1.23i:虚数
“1234”:文字列
‘1’:文字(rune)

エスケープシーケンス

Go言語のエスケープシーケンスは以下のようなものがあります。

\a 警報音

\b バックスペース

\n 復帰改行

\r キャリッジリターン

\f 改ページ

\t タブ

\v 垂直タブ

\\ 文字としての\

\’ シングルクォーテーション(‘)

\” ダブルクォーテーション(“)

\uhhhh Unicode ※hは16進表記

\xhh 16進数の文字コードを持つ文字

変数(var)と定数(const)

var

変数は以下の形式で定義します。

var 変数名 型名

var a int // 初期値がない場合は0(文字列の場合は空文字列)
var b int = 100 // 初期値を設定
var c = 100 // 型を省略すると初期値から推定される

また、以下のようにまとめて定義することも可能です。

var (
    a int // 初期値がない場合は0(文字列の場合は空文字列)
    b int = 100 // 初期値を設定
    c = 100 // 型を省略すると初期値から推定される
)

また、初期値を与える場合は、varを書かずに変数を定義することも可能です。

この場合は:=を用います。

a := 0
b := 100
c, d := 100, "abc"

const

定数はconstで定義します。

constvarと同様にconst (...)でくくることでまとめて定義することも可能です。

const x = 100
const f = 1.0

Go言語の型は以下のようなものがあります。

bool
int8, int16, int32, int64
uint8, uint16, uint32, uint64
float32, float64
complex64, complex128,
byte, rune
uint, int
uintptr, string

型変換について

型名()で型変換を行うことが可能です。

a := (int)100
var b uint
b = (int)a

Go言語は型に対してのチェックが厳しいです。C++などではintとunsigned intなどは、型変換せずに代入可能ですがGo言語ではエラーになります。

演算子

算術演算子

演算子
+a+b加算
a-b減算
*a*b乗算
/a/b除算
%a%b剰余
a–デクリメント(a=a-1)
++a++インクリメント(a=a+1)
&a&bビット毎の論理積(AND)
|a|bビット毎の論理和(OR)
^a^bビット毎の排他的論理和(XOR)
&^a&^bビット毎のAND NOT
<<a<<b左シフト
>>a>>b右シフト

比較演算子

演算子
==a==b等しい
!=a!=b等しくない
<a<b左辺が右辺より小さい
>a>b左辺が右辺より大きい
<=a<=b左辺が右辺以下
>=a>=b左辺が右辺以上

論理演算子

Go言語では、論理演算はbool型に対して行います

演算子
&&a&&b論理積(AND)
||a||b論理和(OR)
!!a否定(NOT)

代入演算子

演算子
=a=b左辺に右辺を代入
:=a := 10var a = 10と同じ
+=, -=, *=, /=, %=a+=bなどa = a+bなどと同じ
&=, |=, ^=, &^=a|=bなどa = a | bなどと同じ
<<=, >>=a <<= ba = a<<bなどと同じ

アドレス演算子

演算子
&&a変数のポインタ
**aポインタ型の定義

レシーバー

チャネル用の演算子です。主にgoroutineで使います。

演算子
<-ch <- 1チャネル用

演算子の優先順

演算子の優先順は以下の通り

優先順位
*, /, %, <<, >>, &, &^
+, -, |, ^
==, !=, <, >, <=, >=
&&
||

演算子は、他の言語も似たような感じなので説明は割愛します。

配列、スライス、マップ

配列(array)

個数が決まっていて変更不可のものを配列と呼びます。配列の定義は以下のようになります。

[]の中に配列の要素数を指定する必要があります。また、配列のインデックスは0始まりです。

なお、初期化とペアで行う場合、個数を...で省略することも可能です。

a := [4]int{}
b := [2]int{2,3}
var c [2]string
c[0] = "aaa"
c[1] = "bbb"
x := [...]float32{1,2,3,4.4}

スライス(slice)

配列に似ていますが、個数が変更可能なものがスライスです。スライスを定義する場合は[]の中に個数を入れません。

宣言は以下のような方法があります。

var a []int // 空のスライス
b := []int{} // 空のスライス
c := []int{1,2,3} // 初期個数が3で値が1,2,3のスライス
c := make([]int, 10) // 初期個数10個を確保
d := make([]int, 0, 10) // 初期個数は0個で、10個分のメモリをあらかじめ確保

スライスに値を追加する場合はappendを使います。同じ型のスライスを後ろに追加したい場合には...bのように変数名に...をつけます

a := []int{}
a = append(a, 1) // 1を末尾に追加
a = append(a, 2) // 2を末尾に追加

b := []int{1,2,3}
a = append(a, ...b) // スライスを後ろに追加する場合は...をつける

配列とスライスの要素へのアクセス

スライスや配列の要素へのアクセスは以下のようになります(以下の説明は先頭は0番目からと考えてください)

  • スライスaのn番目の要素にアクセスしたい場合はa[n]
  • スライスaのi番目からj-1番目の要素にアクセスしたい場合はa[i:j]
  • スライスaのi番目から末尾までの要素アクセスしたい場合はa[i:]
  • スライスaの先頭からi-1番目までの要素アクセスしたい場合はa[:i]
a := []int{0, 1, 2, 3, 4, 5}
fmt.Println(a[1:3])
// [1 2]
fmt.Println(a[:3])
// [0 1 2]
fmt.Println(a[3:])
// [3 4 5]

マップ(map)

辞書(map)は、キーを用いてアクセスすることができるデータ構造です。

宣言は以下のように行います。

以下のようにキーは文字列でもOKです。また、b[0]b[100]を定義した場合は、キー0と100だけ記録されます。

a := map[string]int { "x" : 100, "y" : 10 }
b := make(map[int]bool)
b[0] = true
b[100] = true
fmt.Println(a, b)
// map[x:100 y:10] map[0:true 100:true]

アクセスする場合は以下のようにします。

mapでは定義されているキーに関しては値を、それ以外の場合は初期値を返します。

なお、定義されているかどうかを確認したい場合は、_, ok := ...のようにするとokにtrue/falseでキーが存在するかどうかが返されます。

a := make(map[string]int)
a["x"] = 10
a["y"] = 100

fmt.Println(a["x"])
// 10
fmt.Println(a["z"])
//0

v1, ok1 := a["x"]
fmt.Println(v1, ok1)
// 10 true
v2, ok2 := a["z"]
fmt.Println(v2, ok2)
// 0 false

構文

条件判定(if文)

if文
if 条件 {
  処理
} 

if 条件 {
  処理1
} else {
  処理2
}

if 条件 {
  処理1
} else if 条件 {
  処理2
} 

if 条件 {
  処理1
} else if 条件 {
  処理2
} else {
 処理3
}

Go言語では条件は真理値である必要があります。C++などでは整数が0かどうかで条件分岐する場合はif (整数値) {...}のように記述できますが、Goではif 整数値==0 {...}と書く必要があります。

Go言語のif文の注意事項は以下になります

  • 処理が1行でも必ず{}する必要があります
  • 条件は()で囲む必要はありません
if a > b {
  fmt.Println("a > b")
} else if a < b {
  fmt.Println("a < b")
} else {
  fmt.Println("a == b")
}

Go言語のif文では以下のように条件の前に式を入れることが可能です。

例えば以下のように記述することが可能です。

m := map[int]bool{1: true}
if _, ok := m[1]; ok {
    ....    
}

条件判定(switch文)

switch文
switch 式 {
case 条件1:
  処理1
case 条件2:
  処理2
   :
default:
}

Go言語のif文の注意事項は以下になります

  • C++言語などと異なりbreakは必要ありません。逆に次の条件の処理も実行したい場合はfallthroughを書く必要がある
  • case文に条件文を入れることが可能です
switch stat {
case 0 : 
   fmt.Println("STOP")
case 1 :
   fmt.Println("RUNNING...")
default : 
   fmt.Println("Unknown Status")
}

条件を書く例

switch {
case x > y : 
   fmt.Println("Greater")
case x < y :
   fmt.Println("Less")
default :
   fmt.Println("Equal")
}

繰り返し(for文)

for文
for 開始処理; 条件; 毎回の処理 {
   処理
}

開始処理、条件、毎回の処理は何も書かなくても良いです。以下のようにすると無限ループになります。

for {
}

次のように条件だけ書くとwhile文と同じにります。

x := 12345
for x > 0 {
   x >>= 2
}

一般的なfor文として利用する場合は以下になります。

for i := 0; i < 10; i++ {
  fmt.Println(i)
}

for~rangeによるループ

for文
for インデックス変数, 要素変数 := range 配列やスライス {
   処理
}

rangeを用いてループ処理を行うことも可能です。

a := []int{3,2,5,1,3}
for i, e := range a {
  fmt.Println(i, e)
}

// 0 3
// 1 2
// 2 5
// 3 1
// 4 3

continue, break

ループを抜けたい場合はbreak、途中でループの先頭に戻りたい場合はcontinueを利用します。

for i := 0; i < 10; i++ {
  if i == 3 {
    continue
  }
  if i == 5 {
    break
  }
  fmt.Println(i)
}
// 0
// 1
// 2 ※3はスキップ
// 4 ※5以上はbreakされて終了

ジャンプ(goto文)

goto文
goto ラベル

goto文を使うことで指定のラベルにジャンプすることが可能です。

条件で関数から抜けたり、2重ループから脱出したりするのに使うことが可能です。

	if ok {
		goto END
	}

	for i := 0; i < 100; i++ {
		for j := 0; j < 100; j++ {
			if i+j == 120 {
				fmt.Println("i+j=120")
				goto LOOP_END
			}
		}
	}
LOOP_END:

	fmt.Println("LOOP END")

END:

関数(func)

func文
func 関数名(引数, ...) 戻り値 {
}

Go言語のif文の注意事項は以下になります

  • 関数が複数の値を返す場合は、戻り値を()で括る必要があります。
func add(a, b int) int {
	return a +b
}

func addSub(a, b int) (int, int) {
	return a+b, a-b
}

Go言語では、グローバル変数や構造体、関数が大文字で始まるものはパッケージ外からアクセス可能になります。逆に小文字から始まるものはパッケージ外からアクセスすることができません

結構ハマるので注意が必要です。

構造体(struct)

struct文
sturct {
}

structは以下のような形で利用することが可能です。

var a struct {
	x int
	y string
}

a.x = 1
a.y = "hello"
fmt.Println(a)
// {1, hello}

ただ、typeによる型定義と合わせて以下のようにして利用するのが一般的です。

package main

import "fmt"

type S struct {
	x int
	y string
}

func main() {
	var a S

	a.x = 1
	a.y = "hello"
	fmt.Println(a)
}

ジェネリクス

ジェネリクスがあれば、特定の型に依存しないコードを書くことができる仕組みです。Go1.18から導入された機能になります。Go言語でジェネリクスを利用する場合は以下のように記述します。

ジェネリクス
[型名 型]

以下のAdd関数は、整数型、浮動小数点型、文字列型を入力することが可能です。

func Add[T int | float64 | string](a, b T) T {
	return a + b
}

ジェネリクスについては、以下の記事で詳しく解説しています。

あわせて読みたい
Go言語のジェネリクスの使い方をQueueとSetの実装を例に解説
Go言語のジェネリクスの使い方をQueueとSetの実装を例に解説

高度な使い方

遅延実行(defer)

deferで記述されたものは、関数から戻る直前まで実行が遅延します。

使い方は、ファイルをオープンした直後にCloseを書いておくなどになります。途中でif文でreturnしてもdeferで定義した処理は実行されるので、ファイルのClose忘れなどを防ぐことができます。

func readFile(fname string) ([]string, bool) {
	fp, err := os.Open(fname)
	if err != nil {
		return nil, false
	}
	defer fp.Close()

	ret := []string{}
	//
	// ファイル読み込み
	//

	return ret, true
}

以下のプログラムはdeferの実行例です。startが表示されtendが表示されます。

package main

import (
	"fmt"
)

func main() {
	defer func() {
		fmt.Println("end")
	}()

	fmt.Println("start")
}

// start
// end

ポインタ

Go言語でポインタを利用する場合は、&*を利用します。

変数のポインタを参照する場合は&で、ポインタの中身を参照する場合は*となります。

a := 1
var b *int
b = &a
*b = 2
fmt.Println(a, *b)
// 2 2

メソッド

Go言語はクラスなどが存在しない代わりに、型にメソッドを持たせることができます。

下記の例では、Countableという型にAddResetという関数を定義しています。このように、型に関数を持たせることが可能です。

package main

import (
	"fmt"
)

type Countable int

func (c *Countable) Reset(n int) {
	*c = Countable(n)
}
func (c *Countable) Add() {
	*c = *c + 1
}

func main() {

	c := Countable(0)
	for i := 0; i < 10; i++ {
		c.Add()
	}
	fmt.Println(c)
    // 10

	c.Reset(0)
	fmt.Println(c)
    // 0
}

インタフェース

インタフェース(interface)は、「同じメソッド(機能)を持つ複数の型を、ひとくくりにして扱うための仕組み」です。結構わかりにくいです。

以下、サンプルです。PrintOutという関数はNum, Strのどちらの変数も引数として持つことができます。

package main

import (
	"fmt"
	"strconv"
)

type Printable interface {
	ToStr() string
}

func PrintOut(p Printable) {
	fmt.Println(p.ToStr())
}

type Num struct {
	d int
}

func (dat *Num) Set(idx int) {
	dat.d = idx
}

func (dat Num) ToStr() string {
	return strconv.Itoa(dat.d)
}

type Str struct {
	d string
}

func (dat *Str) Set(s string) {
	dat.d = s
}

func (dat Str) ToStr() string {
	return dat.d
}

func main() {
	v := Num{1}
	var s Str
	s.Set("hello")
	fmt.Println(v, s)

	PrintOut(v)
	PrintOut(s)
}

個人的にはあまり利用しないのでうまく説明できないです。

interface{}型

interface{}型は、any型のようなものです。以下のように定義することでどのような型も受け取ることが可能です。

以下では、PrintOut関数はinterface{}型で引数を受け取り、e.(type)で型を調べ、e.(型名)で型をキャストして表示しています。

このようにinterface{}型を使うことで、さまざまな型を受け取る関数などが作成できます。

package main

import (
	"fmt"
)

func PrintOut(e interface{}) {
	switch e.(type) {
	case int:
		fmt.Println("int =", e.(int))
	case string:
		fmt.Println("string =", e.(string))
	default:
		fmt.Println("Unsupported type.")
	}
}

func main() {
	PrintOut(10)
	PrintOut("hello")
	PrintOut(1.0)
}
// int = 10
// string = hello
// Unsupported type.

ゴルーチン(goroutine)

ゴルーチン(goroutine)Goの特徴的な機能の1つです。これはスレッドによる並列処理を実現するものです。

go func1()と書くだけで、メインの処理は継続しながらfunc1()が呼び出され実行されます。

呼び出した関数とのやりとりはチャネルを利用して行います。また、func1()の終了を待ち合わせる場合にもチャネルが利用できます(sync.WaitGroupを使う方法もあります)。

以下、goroutineを使った生産者・消費者問題のプログラムです。

生産者がn, 消費者が1つです。生産者は並列動作しているので、受け取る順番がバラバラです。

このように手軽に並列を記述できるのもGoの魅力です。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type Pair struct {
	idx, val int
}

func Producer(idx int, ch chan Pair) {
	var r *rand.Rand
	r = rand.New(rand.NewSource(time.Now().UnixNano()))

	for i := 0; i < 10; i++ {
		ch <- Pair{idx, i}
		t := r.Intn(200)
		time.Sleep(time.Duration(t) * time.Millisecond)
	}
	ch <- Pair{idx, -1}
}

func Comsumer(num int, ch chan Pair) {
	tot := 0
	for {
		v := <-ch
		if v.val == -1 {
			tot++
			if tot == num {
				break
			}
		} else {
			fmt.Printf("Producer %d: value %d\n", v.idx, v.val)
		}
	}
}

func main() {
	ch := make(chan Pair, 10)
	n := 3
	for i := 0; i < n; i++ {
		go Producer(i, ch)
	}
	Comsumer(n, ch)
}

goruoutneに関しては、これだけで結構な量になるので、ここでは紹介だけにしておきます。どこかで詳しく記事にしたいと考えています。

あわせて読みたい
Go言語のチャネルの基本と注意点|ゴルーチンでデータを送受信する方法
Go言語のチャネルの基本と注意点|ゴルーチンでデータを送受信する方法

まとめ

ざっとGo言語について紹介しました。

いろいろなサイトで入門という形で紹介している内容ですが、改めてまとめることで自身の整理になりました。

とりあえず、「Go言語ってどんな言語?」と興味を持った方が、なんとなく理解できるように全体を浅く網羅したつもりです。他の言語をマスターしている人にとっては、この程度の情報で十分使い始められると思います。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました