はじめに
Go言語というプログラミング言語があります。最近、今後のプロジェクトでバックエンドにGoを使うのはどうか?という雑談がきっかけで入門してみようかと思いましたので、理解の整理も兼ねてここに書きたいと思います。
事前準備
下記のリンクからGoのインストールを行います。
私の場合、miseというパッケージ管理ツールを用いてGoをインストールしました。環境はWSL2で、homebrew経由でインストールしました。
Hello World
Goのプロジェクトを作成するには、以下のようなコマンドを実行します。
go mod init <モジュール名>
コマンドを実行すると、「go.mod」というファイルが作成されます。
「go.mod」はこのプロジェクトで利用されるモジュールの依存関係を管理するためのルートになるファイルです。
「go.mod」と同じ階層に「main.go」を作成し、以下のようなコードを作成します。
Go言語のソースは何らかのパッケージに属す必要があり、mainパッケージは特別なパッケージとして定義されています。このパッケージはmain関数を含み、プログラム実行時のエントリーポイントとなることを示しています。
「fmt」は標準出力などをサポートするフォーマットのパッケージで、デフォルトで提供されているパッケージです。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
このコードを実行します。以下のコマンドを実行します。このコマンドではビルドと実行を同時に行ってくれるので、見かけではインタプリタ言語を実行しているように見えます。
go run main.go
また、Go言語でソースコードをビルドするには、次のコマンドを実行します。
「-o」オプションでビルドした成果物にこちらで名前を付けることができます。(例のコマンドでは「my-app」になります。)
go build -o my-app main.go
コマンドを実行すると、「my-app」というバイナリの実行ファイルが作成されます。これを実行するには「./my-app」のようにコマンドを打ちます。例の画像では、「Hello, World!」が出力されていることがわかります。
モジュール
モジュールとして、外部からインポートを行う例を説明します。「.env」ファイルを扱うモジュール「godotenv」を利用したいとき、画像のようにimportします。するとインポートでエラーになっています。そのモジュールをまだプロジェクトにダウンロードしていないからです。
モジュールのダウンロードを行うには以下のコマンドを実行します。
このコマンドは必要なモジュールをダウンロードするだけでなく、使っていない不要なモジュールを削除する機能も担っています。
go mod tidy
モジュールをダウンロードとすると、その情報「go.mod」に記載されます。「go.sum」には管理されているモジュールのハッシュが記述されており、改ざんの防止や一貫性の担保に寄与しています。
外部モジュールのダウンロードができたので、コードを実行できるようになりました。その結果「.env」ファイルから値を取得できました。
package main
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
func main() {
godotenv.Load()
fmt.Println(os.Getenv("ENVIRONMENT"))
}
自作したモジュールを利用することもできます。
例のように、usersフォルダの下に「user.go」を作成し、そこでユーザーIDのようなものを作成する関数を作りました。これを「main.go」で利用したいと思います。
package users
import (
"github.com/google/uuid"
)
func GetUser() string {
uuid, err := uuid.NewRandom()
if err != nil {
return "Error generating UUID"
}
return "User" + uuid.String()
}
インポートの際に、自作したモジュールがあるフォルダを指定します。
実行すると機能していることがわかります。
package main
import (
"fmt"
"my-space/users"
)
func main() {
user1 := users.GetUser()
fmt.Println(user1)
}
モジュールとパッケージのイメージについては以下の図のようになります。モジュールは「go.mod」と「go.sum」を持ちます。モジュールは複数のパッケージを持つことができ、それを利用できます。また、gitなどで公開されているモジュールを外部モジュールとしてダウンロードして用いる事ができます。
変数
Go言語の変数定義は次のようになります
- varを用いた宣言
-
var i int
のように、varの後に変数名、型を宣言します。 この方法では型宣言は必須ですが、初期値の設定は必須ではありません。初期値を設定しなかった場合は、型に応じた値が設定されます。例えば、int型は「0」、string型は「""」、bool型は「"false"」となります
-
- := を用いた宣言
-
i := 1
のように宣言をします。この宣言方法では明示的な代入が必須で、型は値に応じて推論されます。また、この宣言方法はトップレベルではエラーになり、関数内で用います。そして宣言した関数の外では利用できないというスコープの違いがあります
-
変数を出力する際に、「%v」で値を「%T」で型を出力することができます。また「%[1]v」などと書くことで何番めの変数を出力するか指定もできます。
package main
import "fmt"
var greeting1 = "Hello, World!"
var greeting2 = "I am a Go program!"
func main() {
fmt.Printf("%[1]v %v type:%[1]T \n", greeting1, greeting2)
}
ポインタ(2025/03/07追記)
プログラミングにおいてポインタは理解が難しい概念の筆頭かと思います。初めてプログラミングに触れたのがC言語で、ポインタでつまずくという経験に共感できる方もおおいと思います。(私がそうでした。)
初めに、ポインタとはメモリ上のアドレスを指し示す変数のことです。
普段変数を使う際は意識しませんが、データを保持するにはその分の容量のメモリが必要で、変数も一定量のメモリを使用します。そのため、変数に値を代入するとき、より細かく言えば変数が確保したメモリ領域に何らかの値が格納されることになります。
メモリ領域は通常byte単位で表現されます。ではある変数が確保しているメモリ領域がどこなのか、それを扱うには変数の前に「&」を付けて実現できます。
実行結果、メモリのアドレスが表示されています。これは変数の1バイト目のメモリアドレスです。
package main
import (
"fmt"
)
func main() {
var i int = 1
fmt.Println(&i)
}
ポインタ型を参照するためには、「*」を付けて表します。例えばvar p *int
のように書くとp
がint型のポインタ変数になります。
コード例を実行してみると、i
のアドレスとp
の値は等しいです。これは、代入式からも想像できます。
&p
で出力されるp
のアドレスはp
の値(i
のアドレス)異なっていることがわかります。p
は独立してアドレス領域を確保し、その値としてi
のアドレスを持っていることがわかります。
また変数の前に「*」を付け*p
のように書くと、そのポインタ型の変数が値として持っているアドレスに格納されいる値を表示することができます。例の場合だと、p
の値はi
のアドレスなので、i
のアドレスに格納されている値、つまり「1」が出力されます。これを「デリファレンス」といいます。
package main
import (
"fmt"
)
func main() {
var i int = 1
fmt.Println("variable i:")
fmt.Println(&i)
var p *int = &i
fmt.Println("variable p:")
fmt.Println(p)
fmt.Println(&p)
fmt.Println(*p)
}
スライス
スライスはvar slice []int
のように[]<型>という書き方で宣言します。
空のスライス宣言時に、varで宣言した場合、nilとの比較がtrueになるが、:=の宣言ではfalseになります。
つまり、下記のコードだとslice1はtrueになり、slice2はfalseになります。
package main
import "fmt"
func main() {
var slice1 []int
slice2 := []int{}
fmt.Println(slice1 == nil, slice2 == nil)
}
また要素数が異なる配列は違う型として認識されるので、直接代入することができません。
要素の追加はappendメソッドを使います。
第一引数で、スライスを指定し、第二引数以降で追加する要素を指定します。
第二引数以降で「スライス名...」と書くことで、異なるスライスを結合可能です。
slice1 = append(slice1, 1, 2, 3)
slice1 = append(slice1, slice2...)
makeによるスライスの初期化
makeメソッドはスライス、マップ、チャネルの初期化を行ってくれる関数です。ここではスライスに関してのみ述べます。スライスの初期化には以下のような構文を用います。
引数はそれぞれスライスの型、要素数の長さ、スライスが確保するメモリ容量です。
slice = make([]int, length, capacity)
例として以下のようなスライスを作成し、長さとキャパシティを確認してみます。
結果として要素数は3(すべて0で初期化されています)とキャパシティが5のスライスが作成されていることがわかります。
package main
import "fmt"
func main() {
var slice1 = make([]int, 3, 5)
fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))
}
また、このように作られたスライスに対してappendで要素を追加していくこともできます。そうしてキャパシティを超えてしまった場合でも、自動的にキャパシティを追加で確保してくれるためエラーにはなりません。ただし、キャパシティを追加する際にスライスに対してメモリの再割り当てが起きて動作が遅くなるかもしれませんので、初めから十分なキャパシティを確保したほうがよいかもしれないです。
下のコードのように、初めに要素数3、キャパシティ5のスライスを作ったのち、appendで追加することによってキャパシティも確保されていることがわかります。
package main
import "fmt"
func main() {
var slice1 = make([]int, 3, 5)
fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))
slice1 = append(slice1, 1, 2, 3, 4, 5)
fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))
slice2 := []int{10, 20, 30}
slice1 = append(slice1, slice2...)
fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))
}
あるスライスを別のスライスにコピーし、コピー先のスライスの要素を変更した場合、コピー元のスライスでも該当する要素の値が変更されます。これは、コピー元、コピー先のスライスが同じメモリを共有しているためです。
package main
import "fmt"
func main() {
var slice1 = []int{1, 2, 3, 4, 5}
slice2 := slice1
fmt.Printf("slice1: %v\n", slice1)
slice2[0] = 100
fmt.Printf("slice1: %v\n", slice1)
fmt.Printf("slice2: %v\n", slice2)
}
異なるメモリ空間を確保したスライスとしてコピーするには、copy
メソッドを用います。
copy(<コピー先のスライス名>, <コピー元のスライス名>)
このようにしてスライスをコピーすることで、コピー先スライスの変更はコピー元に影響を及ぼさなくなります。
マップ
マップの作成は以下のようなコードで作成できます。
m := map[string]float64{}
m := make(map[string]int)
マップのキーを参照した場合、2つの値が返ります。1つがそのキーに対応する値で、もう1つが存在するかしないかのブール値です。そのキーが存在しない場合、値は0
になります。
また、delete
メソッドでマップと消したいキーを指定することで、該当のキー、値ペアの削除が可能です。
package main
import (
"fmt"
)
func main() {
m1 := make(map[string]map[int]string)
m1["Alice"] = map[int]string{
101: "Hello, Alice!",
102: "Welcome back, Alice!",
}
m1["Bob"] = map[int]string{
201: "Hi, Bob!",
202: "Good to see you, Bob!",
}
m2 := map[string]float64{
"pi": 3.14,
"e": 2.71,
}
v, ok := m1["Alice"][101]
if ok {
fmt.Println(v)
}
v2, ok := m2["lambda"]
if ok {
fmt.Println(v2)
} else {
fmt.Println("Key not found")
}
fmt.Println(m1)
delete(m1, "Alice")
fmt.Println(m1)
}
構造体(struct)
構造体type XXX struct
という形式で表現され、例としては以下のコードのようになります。
user1とuser2を見てわかるように、構造体はスライスと異なりお互いが異なるメモリ領域を確保しているので、user2の変更でコピー元のuser1の値が書き換わることはありません。
また、注意点として構造体を別のパッケージでも扱うためには構造体名、フィールド名(NameやAgeの部分)を大文字から始める必要があります。小文字のものはパッケージ外部から利用できません。日本語のものも利用不可です。
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
user1 := User{
Name: "John",
Age: 30,
}
var user2 User = user1
user2.Name = "Nancy"
fmt.Printf("%[1]T %+[1]v %v\n", user1, user1.Name)
fmt.Printf("%[1]T %+[1]v %v\n", user2, user2.Name)
}
インターフェース
インターフェースも型として実装されるもので、次のような形式で定義されます。これがなにかというと、このインターフェースに定義されたメソッドを持つ型をインターフェースを実装しているとして受け入れる」ということを意味します。
type XXX interface{
yyy() int
zzz() float64
.
.
.
}
実際にインターフェースを実装してみた例が以下になります。
「calc」というインターフェースで、2つのメソッドを定義しています。名前から面積と外周を求めるメソッドとわかります。
次に、構造体を2つ定義しています。構造体は円と長方形で、それぞれ半径と横幅・縦幅を持っています。
構造体に対してメソッドを定義しています。メソッドがどの構造体に対応付けられるのか、レシーバーで指定し、メソッドを作成します。
例では、(c Cirle)
という部分がレシーバーです。つまり、(c Cirle)
という構造体に対して、「float64」型の「CalcArea()」というメソッドを実装します、というコードです。メソッド名と返り値の型がインターフェースで定義したメソッドと一致しています。このように、インターフェースの定義と整合するように、構造体に具体的なメソッドを実装していきます。
コード例では、「Circle」と「Rectangle」という構造体がインターフェースを実装しています。Go言語の特徴で、明示的にインターフェースを実装していなくても、構造体が定義されたインターフェースの内容を満たすようにメソッドを実装している場合は、その構造体は該当のインターフェースを実装しているとみなされます。そのため「Circle」と「Rectangle」はインターフェース「Calc」を実装していると言えます。
.
.
.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
func (c Circle) CalcArea() float64 {
return 3.14 * c.Radius * c.Radius
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.
.
.
また、Go言語では標準でStringerインターフェースというものを提供しており、型に対してStrig()
メソッドを実装すると、その型はStringerインターフェースを実装したものをみなされます。このようにすることで、このインターフェースを実装した型がfmt.Println(xxx)
やfmt.Printf("%s\n", xxx)
を用いて出力するときのフォーマットをこちらで定義できます。
package main
import (
"fmt"
)
type Calc interface {
CalcArea() float64
CalcPerimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) CalcArea() float64 {
return 3.14 * c.Radius * c.Radius
}
func (c Circle) CalcPerimeter() float64 {
return 2 * 3.14 * c.Radius
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) CalcArea() float64 {
return r.Width * r.Height
}
func (r Rectangle) CalcPerimeter() float64 {
return 2 * (r.Width + r.Height)
}
func GetResult(a Calc) {
fmt.Println("Area: ", a.CalcArea())
fmt.Println("Perimeter: ", a.CalcPerimeter())
}
func (c Circle) String() string {
return fmt.Sprintf("Circle with radius: %f, Area: %f, Perimeter: %f", c.Radius, c.CalcArea(), c.CalcPerimeter())
}
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle with width: %f, height: %f, Area: %f, Perimeter: %f", r.Width, r.Height, r.CalcArea(), r.CalcPerimeter())
}
func main() {
circle := Circle{Radius: 5}
rectangle := Rectangle{Width: 5, Height: 10}
// Interface
var calc Calc
calc = circle
GetResult(calc)
calc = rectangle
GetResult(calc)
// Interface Stringer
fmt.Printf("%s\n", circle)
fmt.Println(rectangle)
}
実際にコードを実装した結果、上のようになりました。var calc Calc
で宣言したインターフェース型の変数に構造体の「circle」と「rectangle」が代入できていることがわかります。構造体がインターフェースを正しく実装できているためです。
関数
関数はfunc XXXX() {}
という書き方で作ることができます。コード例の「HelloWorld」関数のように、引数や返り値をとらない関数も作成できます。
CalcAdd
関数のように、()
内に、引数とその型を指定し、() int
のように括弧の後に戻り値の型をしているような関数を作成することができます。
package main
import "fmt"
func HelloWorld() {
fmt.Println("Hello World! from main.go")
}
func CalcAdd(a int, b int) int {
return a + b
}
func main() {
HelloWorld()
fmt.Println(CalcAdd(1, 2))
}
無名関数
以下のように名前のない関数を作成することができます。この関数の後ろに()
と書いていますが、これによって無名関数に引数として値を与え(引数がない場合はなにも与えず)即座に関数を実行することができます。
package main
import "fmt"
func main() {
func(a int) {
a = a * a
fmt.Println(a)
}(5)
func() {
fmt.Println("Hello")
}()
}
無名関数は変数に代入することが可能です。例として以下のようなコードを示します。
package main
import "fmt"
func main() {
Calcsquare := func(a int) int {
a = a * a
return a
}
fmt.Println(Calcsquare(5))
fmt.Println(Calcsquare(15))
}
また、無名関数は関数の引数になることもできます。「Testmessage」関数の引数に無名関数を「f」として受けとっています。
package main
import "fmt"
func Testmessage(f func(int) int, a int) {
fmt.Println("Testmessage, id is ", f(a))
}
func main() {
Calcsquare := func(a int) int {
a = a * a
return a
}
Testmessage(Calcsquare, 11)
}
ここでは詳しく述べませんが、関数の返り値として無名関数を指定することも可能です。
制御構文
if
if
での条件分岐と、else if
、else
が使用できます。else if
、else
は直前の}
と同じ行に置く必要があります。改行するとエラーになります。
package main
import (
"fmt"
)
func main() {
i := 5
if i == 0 {
fmt.Println("i is 0")
} else if i >= 1 {
fmt.Println("i is greater than 1")
} else {
fmt.Println("i is less than 0")
}
}
for
forの基本はfor <初期式>; <条件式>; <後処理式>;
で表されます。また、スライスやマップに対してrange
を用いる事ができます。range
で受け取る値は、スライスの場合はインデクス、値であり、マップの場合は、キー、値です。用いない場合は_
で受け取ると未使用でもエラーにならず扱えます。
package main
import (
"fmt"
)
func main() {
sum := 0
numbers := []int{10, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}
for i := 0; i <= 10; i++ {
sum += i
}
fmt.Println(sum)
sumNumbers := 0
for _, v := range numbers {
sumNumbers += v
}
fmt.Println(sumNumbers)
}
switch
switch <式> {~~~~}
で定義されます。case <値>
で値が式の結果と一致した際に、その場合のcase
が実行されます。どのcase
とも一致しない場合はdefault
が実行されますが省略可能です。
package main
import (
"fmt"
)
func main() {
for i := 0; i <= 3; i++ {
switch i {
case 0:
fmt.Println("Zero")
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
default:
fmt.Println("Unknown Number", i)
}
}
}
所感
個人的に、プログラミングは得意でなく、あまりきちんと学習できてきませんでしたが、Goの基本的な書き方は理解しやすかったと感じます。
ただ、並行処理などGoに特徴的な領域についてはまだ触れられてなく、おそらく難しい可と思いますので、丁寧に理解する必要がありそうです。
参考文献