Skip to content

Goの基本構文

Goはシンプルで、読み書きしやすい構文を持つことが特徴です。以下にその主要な要素をまとめます。

Goの設計目標:

  1. シンプルさ: 複雑さを避け、明確で理解しやすい構文
  2. 読みやすさ: コードが自己文書化される
  3. 効率性: コンパイルが速く、実行が速い
  4. 並行処理: 並行処理を簡単に書ける

問題のあるコード(複雑な構文):

// 問題: 複雑で理解しにくい構文(他の言語の例)
// ジェネリクス、メタプログラミング、演算子オーバーロードなど
// が複雑に絡み合っている
// Goの解決: シンプルで明確な構文
func add(a, b int) int {
return a + b
}
// メリット:
// 1. 理解しやすい
// 2. コンパイルが速い
// 3. チーム開発が容易

Goのプログラムはすべてパッケージに属します。実行可能なプログラムの起点となるのは特別なパッケージmainです。

  • package main: プログラムが実行可能であることを示します。
  • import: 他のパッケージの機能を利用するために使います。
package main
import (
"fmt" // I/Oフォーマットを提供
"math" // 数学関数を提供
)

関数はfuncキーワードで定義し、引数と戻り値の型を明示します。

  • func main(): プログラムのエントリーポイントです。
  • 複数の戻り値: Goの関数は複数の値を返すことができ、エラーハンドリングに広く使われます。
// 戻り値がない関数
func sayHello() {
fmt.Println("Hello!")
}
// 複数の戻り値がある関数
func swap(x, y string) (string, string) {
return y, x
}
  • var: 変数を宣言します。型を明示するか、コンパイラに推論させることができます。
  • :=: 関数内でのみ使える簡潔な変数宣言と初期化の方法です。
  • const: コンパイル時に値が確定する定数を宣言します。
var name string = "Alice"
age := 30
const PI = 3.14

Goのデータ型は静的で、コンパイル時にチェックされます。

  • 配列 (Array): 固定長のデータ型です。サイズは型の一部となります。
  • スライス (Slice): 可変長のデータ型で、内部的には配列を参照します。メモリ効率が良く、ほとんどのGoプログラムで使われます。
  • マップ (Map): キーと値のペアを格納するデータ型です。
  • 構造体 (Struct): 異なる型のデータをまとめて一つの単位として扱うための複合型です。他の言語のクラスに似ています。
// 配列の例 (固定長)
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1]) // Hello World
// スライスの例 (可変長)
s := []int{1, 2, 3}
s = append(s, 4, 5) // 要素の追加
fmt.Println(s) // [1 2 3 4 5]
// マップの例
m := make(map[string]int)
m["Apple"] = 100
m["Banana"] = 200
fmt.Println(m["Apple"]) // 100
// 構造体の例
type Person struct {
Name string
Age int
}
p := Person{Name: "Bob", Age: 25}
fmt.Println(p.Name) // Bob

Goの制御構造は非常にシンプルです。

  • if: 条件分岐に使います。括弧()は不要ですが、中括弧{}は必須です。
  • for: Goにはwhile文がなく、すべてのループはfor文で記述します。
  • switch: 複数の条件を簡潔に記述でき、明示的なbreakは不要です。
// if/elseの例
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd") // 実行される
}
// forループの例
sum := 0
for i := 1; i <= 10; i++ {
sum += i
}
fmt.Println(sum) // 55
// switchの例
i := 2
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two") // 実行される
case 3:
fmt.Println("three")
}

Goはポインタをサポートしますが、C言語とは異なり、算術演算はできません。

  • &: 変数のメモリアドレスを取得します。
  • *: ポインタが指す先の値にアクセスします。
// ポインタの例
i := 10
p := &i // iのメモリアドレスをポインタpに代入
fmt.Println(*p) // pが指す値(10)を出力
*p = 20 // pが指すiの値を20に変更
fmt.Println(i) // 20

Goの並行処理は言語レベルで組み込まれています。

  • Goroutine: 軽量なスレッドで、goキーワードを使って関数を非同期で実行します。
  • Channel: 複数のGoroutine間で安全にデータをやり取りするためのパイプです。Goの設計思想である「通信によってメモリを共有する」を実現します。
import (
"fmt"
"time"
)
// Goroutineの例
func say(s string) {
for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // バックグラウンドで実行
say("hello") // メインのゴルーチンで実行
// Channelの例
messages := make(chan string)
go func() { messages <- "ping" }() // 別ゴルーチンでチャネルに値を送信
msg := <-messages // メインゴルーチンでチャネルから値を受信
fmt.Println(msg) // ping
}

ご提示いただいたGoの基本構文は、非常に簡潔で分かりやすくまとめられています。Go言語の特徴であるシンプルさと実用性がよく伝わってきます。パッケージ、関数、変数といった基本的な要素から、スライス、マップ、構造体といった複合型、そしてGoの最大の強みである並行処理まで、主要な概念を網羅しています。

このガイドに加えて、さらにGoの理解を深めるために、以下のトピックを追加で補足することをおすすめします。

Goには例外処理の機構がありません。代わりに、関数が複数の戻り値を返す機能を利用し、慣例として最後の戻り値でerror型を返します。このシンプルで明示的な方法は、エラーを無視することを防ぎ、堅牢なアプリケーションの構築を促します。

エラーハンドリングを伴う関数呼び出しの例です。

package main
import (
"fmt"
"strconv" // 文字列と数値の変換を提供
)
func main() {
// 文字列を数値に変換する関数
i, err := strconv.Atoi("42")
if err != nil {
// エラーが発生した場合
fmt.Println("文字列を数値に変換できませんでした:", err)
return
}
// エラーがnil(存在しない)の場合
fmt.Println("変換された数値:", i) // 変換された数値: 42
}

この構文は、Goのコード全体で頻繁に見られます。エラーをチェックして適切に処理することが、Goプログラミングの基本です。

Goのインターフェースは、他の言語のように明示的な実装宣言が不要な、ユニークな特徴を持っています。ある構造体がインターフェースのすべてのメソッドを実装していれば、自動的にそのインターフェースを満たすと見なされます。この「ダックタイピング」の考え方は、Goの柔軟性と再利用性を高めます。

Shaperインターフェースを定義し、CircleRectangle構造体がこれを満たす例です。

package main
import "fmt"
// `Shaper`インターフェースは`Area()`メソッドを持つ
type Shaper interface {
Area() float64
}
// `Circle`は`Shaper`インターフェースを満たす
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
// `Rectangle`も`Shaper`インターフェースを満たす
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
c := Circle{Radius: 5}
r := Rectangle{Width: 3, Height: 4}
// インターフェースの変数を宣言
var s Shaper
s = c
fmt.Println("円の面積:", s.Area())
s = r
fmt.Println("長方形の面積:", s.Area())
}

このように、インターフェースを使うことで、異なる型を持つオブジェクトを共通の振る舞いを通じて扱うことができます。

構造体に関連付けられた関数をメソッドと呼びます。メソッドは、funcキーワードと関数名の間にレシーバを記述することで定義します。

  • 値レシーバ: レシーバのコピーに対して操作を行います。元の値は変更されません。
  • ポインタレシーバ: レシーバのポインタ(メモリアドレス)に対して操作を行います。元の値を直接変更できます。

Wallet構造体と、値レシーバおよびポインタレシーバを持つメソッドの例です。

package main
import "fmt"
type Wallet struct {
Balance int
}
// 値レシーバ: ウォレットの残高を返す
func (w Wallet) GetBalance() int {
return w.Balance
}
// ポインタレシーバ: ウォレットの残高を直接変更
func (w *Wallet) Deposit(amount int) {
w.Balance += amount
}
func main() {
myWallet := Wallet{Balance: 100}
// 値レシーバの呼び出し
fmt.Println("現在の残高:", myWallet.GetBalance()) // 100
// ポインタレシーバの呼び出し
myWallet.Deposit(50)
fmt.Println("預金後の残高:", myWallet.GetBalance()) // 150 (値が変更されている)
}

ユースケース1: エラーハンドリングのパターン

Section titled “ユースケース1: エラーハンドリングのパターン”

実践的なエラーハンドリング:

// カスタムエラーの定義
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// エラーのラッピング
func processUser(userID int) error {
user, err := getUser(userID)
if err != nil {
return fmt.Errorf("failed to process user %d: %w", userID, err)
}
if err := validateUser(user); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
return nil
}
// エラーの型アサーション
func handleError(err error) {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// バリデーションエラーの処理
fmt.Printf("Validation error: %s\n", validationErr.Message)
} else {
// その他のエラーの処理
fmt.Printf("Unexpected error: %v\n", err)
}
}

ユースケース2: インターフェースによる抽象化

Section titled “ユースケース2: インターフェースによる抽象化”

実践的なインターフェースの使用:

// ストレージの抽象化
type Storage interface {
Save(key string, value []byte) error
Get(key string) ([]byte, error)
Delete(key string) error
}
// メモリストレージの実装
type MemoryStorage struct {
data map[string][]byte
}
func (m *MemoryStorage) Save(key string, value []byte) error {
m.data[key] = value
return nil
}
func (m *MemoryStorage) Get(key string) ([]byte, error) {
value, ok := m.data[key]
if !ok {
return nil, fmt.Errorf("key not found: %s", key)
}
return value, nil
}
func (m *MemoryStorage) Delete(key string) error {
delete(m.data, key)
return nil
}
// ファイルストレージの実装
type FileStorage struct {
basePath string
}
func (f *FileStorage) Save(key string, value []byte) error {
return os.WriteFile(filepath.Join(f.basePath, key), value, 0644)
}
func (f *FileStorage) Get(key string) ([]byte, error) {
return os.ReadFile(filepath.Join(f.basePath, key))
}
func (f *FileStorage) Delete(key string) error {
return os.Remove(filepath.Join(f.basePath, key))
}
// ストレージに依存しないサービス
type UserService struct {
storage Storage
}
func (s *UserService) SaveUser(user *User) error {
data, err := json.Marshal(user)
if err != nil {
return err
}
return s.storage.Save(fmt.Sprintf("user-%d", user.ID), data)
}
// 使用例: 実装を簡単に切り替え可能
func main() {
// メモリストレージを使用
memoryStorage := &MemoryStorage{data: make(map[string][]byte)}
service1 := &UserService{storage: memoryStorage}
// ファイルストレージを使用
fileStorage := &FileStorage{basePath: "/tmp/storage"}
service2 := &UserService{storage: fileStorage}
// 同じインターフェースで動作
service1.SaveUser(&User{ID: 1, Name: "Alice"})
service2.SaveUser(&User{ID: 2, Name: "Bob"})
}

ユースケース3: ポインタの実践的な使い分け

Section titled “ユースケース3: ポインタの実践的な使い分け”

値レシーバ vs ポインタレシーバ:

// 値レシーバを使用すべき場合:
// 1. メソッドがレシーバを変更しない
// 2. レシーバが小さな構造体
type Point struct {
X, Y int
}
func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
// ポインタレシーバを使用すべき場合:
// 1. メソッドがレシーバを変更する
// 2. レシーバが大きな構造体
type User struct {
ID int
Name string
Email string
// ... 多くのフィールド
}
func (u *User) UpdateEmail(email string) {
u.Email = email // レシーバを変更するため、ポインタレシーバが必要
}
func (u *User) UpdateName(name string) {
u.Name = name
}
// 実践的な判断基準:
// - 小さな構造体(< 10フィールド): 値レシーバでもOK
// - 大きな構造体(> 10フィールド): ポインタレシーバを推奨
// - メソッドがレシーバを変更する: ポインタレシーバが必須

これらの要素は、Goでより複雑なアプリケーションを構築する上で不可欠な概念です。特にエラーハンドリングは、Goのコーディングスタイルを特徴づける重要な部分であり、必ず押さえておくべきトピックです。

シニアエンジニアとして考慮すべき点:

  1. エラーハンドリング: エラーを無視せず、適切に処理する
  2. インターフェースの設計: 小さなインターフェースを好む(インターフェース分離の原則)
  3. ポインタの使い分け: 値レシーバとポインタレシーバの適切な使い分け
  4. 並行処理: GoroutineとChannelの適切な使用