こんにちは!
普段はPythonやTypescriptを書いているITエンジニア4年目の私が、Googleが開発したプログラミング言語「Go言語(通称: Golang)」を学んでみましたので、その魅力をご紹介します。
Go言語は、シンプルさと効率性を重視して設計されており、近年多くのWebサービスやインフラ系のツールで採用されています。この記事では、Go言語の基本的な文法を紹介するのではなく、普段あまり触れないポインタのような概念、そしてGo言語の真骨頂ともいえる機能まで、他言語を書いている私が学んだことを分かりやすくまとめてみました!
Go言語の基本と特徴
Go言語に初めて触れて感じたのは、何と言ってもそのシンプルさです。構文はC言語やJavaに似ている部分もありますが、複雑な機能は意図的に排除されていて、誰が書いても似たようなコードになるように工夫されています。これにより、コードの読み書きがとてもしやすくなっているんですね。
クラスがないって本当? 🤔
そうなんです。Go言語にはJavaやC++のような class というキーワードがありません。その代わりに、struct (構造体) と interface(インターフェース) という仕組みを使って、オブジェクト指向のような考え方を実現しています。
1. struct(構造体):データのまとまりを作る
struct は、複数のデータを一つにまとめるための型です。他の言語でいうクラスの「フィールド」や「プロパティ」のようなものだとイメージすると分かりやすいですよ。
// Userという名前のstruct(構造体)を定義
// 名前(string)と年齢(int)というデータを持つ
type User struct {
Name string
Age int
}
func main() {
// User構造体のインスタンス(実体)を作成
// var user User
// user.Name = "Alice"
// user.Age = 30
user := User{Name: "Alice", Age: 30}
// 構造体のデータにアクセス
fmt.Println(user.Name) // 出力: Alice
fmt.Println(user.Age) // 出力: 30
}
2. method(メソッド):データに振る舞いを加える
method は、struct に関連付けられる関数のことです。これにより、データとそのデータに対する操作を一緒に管理できます。
先ほどのUser構造体に、自己紹介をするSayHelloメソッドを追加してみましょう。
package main
import "fmt"
type User struct {
Name string
Age int
}
// User構造体に関連付けられたメソッドを定義
// func (u User) ... の (u User) が「レシーバー」
// このメソッドがどのstructに関連付けられているかを示す
func (u User) SayHello() {
fmt.Printf("Hello, my name is %s and I am %d years old.\n", u.Name, u.Age)
}
func main() {
user := User{Name: "Bob", Age: 25}
// structのインスタンスを通じてメソッドを呼び出す
user.SayHello() // 出力: Hello, my name is Bob and I am 25 years old.
}
3. interface (インターフェース) - 振る舞いを抽象化する
interfaceは、メソッドの「名前」と「引数」「戻り値」の型だけを定義するものです。interfaceを満たすstructは、そのinterfaceの型として扱うことができます。これは、他の言語の「抽象クラス」や「インターフェース」に似た、ポリモーフィズム(多様性)を実現する機能です。
例: Greeterというインターフェースを定義し、SayHelloメソッドを持つことを要求してみましょう。
package main
import "fmt"
// Greeterという名前のinterface(インターフェース)を定義
// SayHello()というメソッドを持つことを要求
type Greeter interface {
SayHello()
}
type User struct {
Name string
Age int
}
// User構造体にSayHelloメソッドを定義
// これにより、UserはGreeterインターフェースを満たす
func (u User) SayHello() {
fmt.Printf("Hello, my name is %s.\n", u.Name)
}
// Greeterインターフェースを引数に取る関数
func Greet(g Greeter) {
g.SayHello()
}
func main() {
user := User{Name: "Charlie", Age: 40}
// User型をGreeterインターフェースとしてGreet関数に渡せる
Greet(user) // 出力: Hello, my name is Charlie.
}
Greet関数の引数はUser型ではなく、Greeterインターフェースになっています。これにより、Greet関数はSayHello()メソッドを持つどんな型でも受け入れることができます。
レシーバーって何?値とポインタの違い
メソッドを定義するときに出てきた (p Product) の部分がレシーバーです。これは、そのメソッドがどの型のためのものかを示しています。レシーバーには「値レシーバー」と「ポインタレシーバー」の2種類があり、これがGo言語を理解する上でとても重要なポイントになります。
値レシーバー (p Product)
メソッドを呼び出すと、レシーバーのコピーが作られて渡されます。そのため、メソッド内で値を変更しても、呼び出し元のオリジナルデータには影響しません。先ほどの Sell メソッドがこの例ですね。
ポインタレシーバー (p *Product)
メソッドを呼び出すと、レシーバーの**メモリアドレス(ポインタ)**が渡されます。これにより、メソッド内で値を変更すると、呼び出し元のオリジナルデータも直接変更されます。在庫を実際に減らすにはこちらを使います。
両方の違いをコードで見てみましょう!
// 商品の情報をまとめるための型を定義します
type Product struct {
Name string // 商品の名前
Price int // 値段
Stock int // 在庫数
}
// ポインタレシーバーを使うと、元の値を変更できます
func (p *Product) SellActual(quantity int) {
if p.Stock >= quantity {
p.Stock -= quantity // pはポインタなので、元のProductの在庫が減ります!
fmt.Printf("%sを%d個販売しました。残り在庫: %d\n", p.Name, quantity, p.Stock)
} else {
fmt.Printf("%sは在庫が足りません。\n", p.Name)
}
}
func main() {
// 「みかん」という商品を作ります
orange := Product{Name: "みかん", Price: 120, Stock: 20}
fmt.Printf("--- 操作前の在庫: %d ---\n", orange.Stock) // 出力: 20
// (1) 値レシーバーのメソッドを呼び出し
orange.Sell(5)
fmt.Printf("値レシーバー実行後の在庫: %d (変わらない!)\n\n", orange.Stock) // 出力: 20
// (2) ポインタレシーバーのメソッドを呼び出し
orange.SellActual(5)
fmt.Printf("ポインタレシーバー実行後の在庫: %d (減った!)\n", orange.Stock) // 出力: 15
}
このように、元の値を変更したい場合はポインタレシーバーを使うと覚えておくと良いですね!
* と & を徹底解説!ポインタの基本 📍
Go言語ではポインタという概念がとても重要です。少し難しく感じるかもしれませんが、コンピュータのメモリを「住所付きの引き出しが並んだ倉庫」に例えると分かりやすいですよ。
-
値: 引き出しに入っている「リンゴ」や「150円」といった中身そのもの。
-
アドレス: 「A-1番地の引き出し」のような、引き出しの場所(住所)。
-
ポインタ: 引き出しの住所が書かれたメモ。このメモ自体には中身はなく、場所の情報だけが書かれています。
Go言語では、これらの概念を & と * という記号で扱います。
&(アンパサンド)演算子:住所を知る
変数名の前に付けると、その変数が保存されているメモリアドレスを取得できます。
price := 150
fmt.Println(price) // 出力: 150 (値)
fmt.Println(&price) // 出力: 0xc0000b4008 (priceのアドレス。実行ごとに変わります)
*(アスタリスク)演算子:ポインタの宣言と、中身を見る
*(アスタリスク)には2つの使い方があるので注意が必要です。
-
ポインタ型の宣言: var p *int のように型として使うと、「この変数はint型の値が格納されているアドレス(ポインタ)を保持しますよ」という意味になります。
-
デリファレンス: ポインタ変数の前に付けると、「このポインタが指している住所にある**中身(値)**を見せてください」という意味になります。
stock := 10 // 値10を格納する変数
ptrStock := &stock // stock変数のアドレスを保持するポインタ変数 (*int型)
fmt.Println(ptrStock) // 出力: 0xc0000b4018 (stockのアドレス)
fmt.Println(*ptrStock) // 出力: 10 (ptrStockが指すアドレスにある値)
// ポインタ経由で元の値を変更する
*ptrStock = 5
fmt.Println(stock) // 出力: 5 (元のstockの値も変わりました!)
構造体のポインタとシンタックスシュガー
Go言語には、構造体のポインタを扱う際に便利な「シンタックスシュガー」が用意されています。これがあるおかげで、コードがとてもシンプルになります。
// Product構造体へのポインタを作成します
applePtr := &Product{Name: "リンゴ", Price: 150, Stock: 10}
// 本来、ポインタが指す中身のフィールドにアクセスするにはこう書きます
fmt.Println((*applePtr).Name) // 出力: リンゴ
// しかしGoでは、このように直接アクセスできます!これがシンタックスシュガーです。
fmt.Println(applePtr.Name) // 出力: リンゴ (同じ結果で、書きやすい!)
// 値の変更も直接できます
applePtr.Stock = 50
fmt.Println(applePtr.Stock) // 出力: 50
Go言語を支える重要なルールと機能
Go言語には、コードをクリーンで堅牢に保つための、特徴的な機能やルールがいくつかあります。
nil って何のこと?
nil は、他の言語の null や JavaScriptの undefined に似た概念です。ポインタ、スライス、マップ、インターフェースなどの型において「値が存在しない・初期化されていない」状態を示すゼロ値として使われます。
Goでは関数がエラーを返す際、エラーがなければ nil を返すのが一般的です。そのため、if err != nil というコードを頻繁に目にします。これはGoの標準的なエラーハンドリング方法なんですね。
名前のルールがアクセス範囲を決める(エクスポート)
Go言語には、変数名や関数名の付け方で、他のパッケージからアクセスできるかどうかを決めるという明確なルールがあります。
-
大文字で始まる名前 (例: PublicFunction): 公開(Exported) され、他のパッケージからアクセスできます。
-
小文字で始まる名前 (例: privateFunction): 非公開(Unexported) となり、同じパッケージ内からしかアクセスできません。
このシンプルなルールのおかげで、外部に公開するAPIと内部だけで使う処理が名前を見るだけで区別でき、コードの可読性が大きく向上します。
並行処理の主役「ゴルーチン」
Go言語の最大の魅力の一つが、ゴルーチン(Goroutine) による簡単な並行処理です。関数の呼び出しの前に go キーワードを付けるだけで、その関数を非同期で実行できます。
ゴルーチンはOSのスレッドよりもはるかに軽量なため、何万、何十万もの処理を同時に動かすことが簡単にできてしまいます。これにより、パフォーマンスの高いサーバーなどを効率的に開発できるのです。
go func() {
// do something
}()
for文だけで何でもできる!
Goの for 文は非常に柔軟で、他の言語の for, while, for-each の役割をすべて一つでこなせます。
// 1. C言語風の基本的なループ
for i := 0; i < 3; i++ {
fmt.Println(i)
}
// 2. 条件のみ (while文のように)
count := 0
for count < 3 {
fmt.Println("Count:", count)
count++
}
// 3. 無限ループ (breakで抜ける)
for {
fmt.Println("Loop!")
break
}
// 4. rangeを使った繰り返し (for-eachのように)
fruits := []string{"apple", "banana"}
for index, fruit := range fruits {
fmt.Printf("%d: %s\n", index, fruit)
}
エラーは戻り値で返す文化
Goには try-catch 例外処理がありません。その代わりに、関数がエラーを戻り値として返すのが基本です。
file, err := os.Open("filename.txt")
if err != nil {
// errがnilでなければエラーが発生したということ
// ここでエラーに応じた処理をします
log.Fatal(err)
}
// errがnilなら、fileを安全に使えます
この設計により、エラー処理を忘れることが少なくなり、より堅牢なプログラムを書きやすくなっています。
まとめ
Go言語は、学ぶほどにそのシンプルでパワフルな設計に魅了される言語だと感じました。特に、軽量なゴルーチンによる並行処理、ポインタを扱いやすくするシンタックスシュガー、そして明示的なエラーハンドリングの文化は、現代のソフトウェア開発において非常に強力な武器になると確信しています。
学ぶべきことはまだまだたくさんありますが、この記事が皆さんのGo言語への第一歩の助けになれば嬉しいです。私も、これからさらに深く学んでいきたいと思います!
最後まで読んでいただき、ありがとうございました!✨
AI辞書でわからない単語を一瞬で検索できる生産性爆上げツールを公開しているのでぜひ!
