Gemcook Tech Blog
🧗‍♂️

他言語プログラマがGo言語の基本をキャッチアップする

に公開

2025年6月より、Gemcookという会社でバックエンドエンジニアとして働いています。
村中(@wage790)です。

これまではRailsを中心に開発してきましたが、Gemcookに入社してからGo言語(Golang)を扱うことになり、現在必死にキャッチアップしています。

アウトプットしながらインプットの学習をするのが効果的だと考え、簡単なアプリを作りながらGoの基本文法を習得することにしました。以下の記事に書かれている基礎文法を、1つのアプリ開発を通じて網羅できるように構成しています。

https://qiita.com/tfrcm/items/e2a3d7ce7ab8868e37f7

記事内のトピックとアプリでの実装・学習ポイントの対応は以下のとおりです。

目的

他言語プログラマがgolangの基本を押さえる為のまとめ」の記事に掲載されている、Goの主要文法(スライス、ポインタ、メソッド、interface、goroutine、channelなど)を、実際の簡単なアプリ開発を通じて習得すること。

これから作るアプリの概要


大阪グルメ検索アプリ

Gemcookは大阪に本社があるため、大阪をテーマにしたアプリにしました。

  • Goで大阪の名物グルメをAPI化
  • Reactで一覧&検索フォームを表示
  • グルメ名で検索
  • goroutineで詳細データ取得を並行処理

ディレクトリ構成

osaka-gourmet-app/
├── backend/                             
│   ├── go.mod                           
│   ├── main.go               # アプリのエントリーポイント(サーバー起動・CORS設定)
│   └── gourmet/                         
│       ├── model.go          # Gourmet構造体とメソッド(ポインタ・構造体)
│       ├── repo.go           # グルメデータの保存・検索処理(スライス・インターフェース)
│       ├── service.go        # 詳細情報取得処理(goroutine + channel)
│       ├── handler.go        # APIエンドポイント定義(Ginのルーティング処理)
│       └── model_test.go     # model.go のユニットテスト(ポインタ・テストの基本)
└── frontend/                           
    └── React + TypeScript              

バックエンドはGoでAPIを構築し、それをフロントエンドに表示させる構成です。

① Goモジュールを初期化

cd backend
go mod init osaka-gourmet-backend
go get github.com/gin-gonic/gin

Goのフレームワークであるginを導入します。
https://gin-gonic.com/ja/

必要なファイルも作成しておきます。

mkdir gourmet
cd gourmet
touch model.go repo.go service.go handler.go model_test.go

② gourmet/model.go - 構造体・メソッド・ポインタ

構造体・メソッド・ポインタを扱います。

グルメというデータ構造と、それを操作する基本的なメソッドを定義するファイルを作成します。

gourmet/model.go
package gourmet

// Gourmet 大阪グルメの情報を表す構造体
type Gourmet struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// Rename グルメの名前を変更する
func (g *Gourmet) Rename(newName string) {
	g.Name = newName
}

Goには、オブジェクト指向言語における class(クラス)の概念はありません。その代わりに、関連する情報をひとまとめにする struct(構造体)と、構造体に紐づけて動作を定義するメソッドを組み合わせて使います。

ここでは、Gourmetという構造体がグルメのIDとNameを持つデータ構造として定義されており、Renameメソッドによってグルメ名を変更できるようにしています(ただし、このメソッドはアプリ上の機能としては実装せず、テストのみに使用します)。

Renameメソッドの (g *Gourmet)はレシーバと呼ばれ、このメソッドがGourmet構造体に属していることを示します。gはレシーバ名(変数)であり、慣習的に1〜2文字の短い名前が使われます。JavaScriptにおけるthisに近いものです。

Goのレシーバには2種類あります。

  • 値レシーバ (g Gourmet)

    • 構造体のコピーを扱う
    • 元のデータは変更されない
  • ポインタレシーバ (g *Gourmet)

    • 構造体の参照(ポインタ)を扱う
    • 元のデータに変更が反映される

今回は名前を上書き保存したいため、ポインタレシーバを使用します。*Gourmet*はポインタ(参照)を意味しており、gGourmetのコピーではなく元のデータそのものを指します。そのため、g.Name = newNameと書くことで、構造体の中身を直接上書きすることができます。

構造体がないと困ること(構造体のメリット)

構造体がなければ、複数の値を1つにまとめて扱えないため、以下のような問題が発生します。

❌関数の引数・戻り値がごちゃごちゃになる

// 構造体がない場合(悪い例)
func sendMail(id int, name string, email string) {
  // ...
}

// 構造体を使えば
func sendMail(u User) {
  // ...
}

→ パラメータが多いと管理や可読性が悪化します。

❌APIのリクエストやレスポンスを扱えない

たとえば、JSONのPOSTデータを受け取る場合、

{
  "name": "Taro",
  "email": "taro@example.com"
}

構造体がなければ、このようなデータをうまく受け取って処理することができません。構造体があれば、以下のように受け取れます。

type SignupRequest struct {
  Name  string `json:"name"`
  Email string `json:"email"`
}

func handleSignup(c *gin.Context) {
  var req SignupRequest
  if err := c.BindJSON(&req); err != nil {
    c.JSON(400, gin.H{"error": "Invalid request"})
    return
  }
  // req.Name や req.Email を使える
}

https://qiita.com/ryo_manba/items/c567858befd04602e3ec

https://zenn.dev/ak/articles/1fb628d82ed79b?redirected=1

③ gourmet/repo.go - スライス・if・インタフェース

スライス・if・インターフェースを扱います。

グルメデータの取得方法をインターフェースとして抽象化し、データをメモリ上のスライスに保持・検索できる仕組みを定義するファイルを作成します。

gourmet/repo.go
package gourmet

import "strings"

// GourmetRepository グルメ情報のリポジトリインターフェース
type GourmetRepository interface {
	FindAll() []Gourmet
	FindByKeyword(keyword string) []Gourmet
}

// MemoryGourmetRepo メモリ上でグルメ情報を管理するリポジトリ
type MemoryGourmetRepo struct {
	data []Gourmet
}

// NewMemoryRepo 新しいメモリリポジトリを作成し、大阪グルメデータで初期化する
func NewMemoryRepo() *MemoryGourmetRepo {
	return &MemoryGourmetRepo{
		data: []Gourmet{
			{1, "たこ焼き"},
			{2, "お好み焼き"},
			{3, "串かつ"},
			{4, "かすうどん"},
			{5, "ねぎ焼き"},
			{6, "いか焼き"},
			{7, "肉吸い"},
			{8, "ホルモン焼き"},
			{9, "きつねうどん"},
			{10, "どで焼き"},
			{11, "てっちり"},
			{12, "豚まん"},
			{13, "オムライス"},
			{14, "高井田ラーメン"},
			{15, "金時人参"},
			{16, "バッテラ"},
		},
	}
}

// FindAll 全てのグルメ情報を取得する
func (r *MemoryGourmetRepo) FindAll() []Gourmet {
	return r.data
}

// FindByKeyword キーワードでグルメ情報を検索する
func (r *MemoryGourmetRepo) FindByKeyword(keyword string) []Gourmet {
	var result []Gourmet
	for _, g := range r.data {
		if keyword == "" || contains(g.Name, keyword) {
			result = append(result, g)
		}
	}
	return result
}

// contains 文字列に指定されたキーワードが含まれているかチェックする
func contains(name, keyword string) bool {
	return strings.Contains(name, keyword)
}

Goでは、処理の仕組みを柔軟にするために、インターフェースを用いて抽象化を行います。ここではFindAll()FindByKeyword()の2つのメソッドを持つGourmetRepositoryインターフェースを定義します。

https://zenn.dev/kasa/articles/golang-interface

構造体Gourmetのデータは、Goのスライスという動的配列に格納しています。

検索処理では、ループとif文を組み合わせて、グルメ名にキーワードが含まれているかを判定します。キーワードが空文字の場合は全件を対象とし、contains関数で部分一致検索を行います。

文字列の検索処理は、contains関数として分離しています。これは、1つの関数が1つの責務を持つという設計上の基本に沿ったものです。

④ gourmet/service.go - goroutine + channel

goroutineとchannelを扱います。

goroutineは非同期に関数を実行するための仕組みで、channelはその処理結果を受け渡すための通信手段です。goroutineは「ゴルーチン」、channelは「チャネル」と読みます。

gourmet/service.go
package gourmet

import (
	"fmt"
	"time"
)

// FetchDetails 指定されたグルメリストの詳細情報を並行処理で取得する
func FetchDetails(gourmets []Gourmet) []string {
	ch := make(chan string) // channelを作成。string型の値を送受信できる。

	// 各グルメごとにgoroutineを起動して非同期に処理
	for _, s := range gourmets {
		go func(name string) {
			time.Sleep(500 * time.Millisecond)                   // 疑似的に時間がかかる処理を再現
			msg := fmt.Sprintf("✅ %sは大阪の定番!一度は食べたい逸品です🍽️", name) // 結果をchannelへ送信
			ch <- msg
		}(s.Name)
	}

	var results []string
	// 全てのgoroutineの結果を受け取る(channelの受信)
	for range gourmets {
		results = append(results, <-ch)
	}
	return results
}

Goにおけるgoroutineは、go 関数名()の形式で記述することで非同期に処理を実行できます。

上記のコードでは、forループの中で各グルメに対して匿名関数(中身の処理は定義されているけど関数名が付けられていない状態の関数)をgoroutineとして実行しています。この書き方により、ループ内の処理が並列に実行され、すべてのグルメに対するメッセージ生成処理が同時に進行するため、全体の待ち時間が短縮されます。

本来はAPIアクセスや外部サービスとの通信に時間がかかる想定ですが、ここでは疑似的にtime.Sleepを使って処理が重いケースを再現しています。

goroutineは非同期で動作するため、「いつ終わったか」「どんなデータを返したか」をそのままでは知ることができません。そこで使うのがchannelです。goroutineがデータをch <-(送信)し、メイン側で<-ch(受信)することで、処理の完了と結果をやり取りします。

https://zenn.dev/farstep/articles/f712e05bd6ff9d

⑤ gourmet/handler.go - APIルーティング

ここでは、GoのWebフレームワークであるGinを使って、APIのルーティング処理を実装します。APIルーティングとは、URLのパスと、それに対応する処理を紐づけることです。

このファイルでは、GETとPOSTのリクエストを受け取り、グルメ情報の取得や検索、詳細情報の取得を行うエンドポイントを定義しています。

gourmet/handler.go
package gourmet

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// エラーレスポンスを統一化
func sendErrorResponse(c *gin.Context, status int, message string) {
	c.JSON(status, gin.H{"error": message})
}

// RegisterRoutes グルメAPIのルートを登録する
func RegisterRoutes(r *gin.Engine, repo GourmetRepository) {
	// グルメ一覧を取得するエンドポイント(キーワード検索対応)
	r.GET("/gourmets", func(c *gin.Context) {
		keyword := c.Query("keyword")
		data := repo.FindByKeyword(keyword)
		c.JSON(http.StatusOK, data)
	})

	// 検索結果だけの詳細を取得
	r.POST("/details", func(c *gin.Context) {
		var gourmets []Gourmet
		if err := c.BindJSON(&gourmets); err != nil {
			sendErrorResponse(c, http.StatusBadRequest, "リクエスト形式が不正です")
			return
		}
		details := FetchDetails(gourmets)
		c.JSON(http.StatusOK, details)
	})
}
  • GET /gourmets:グルメ一覧(キーワード検索付き)

クエリパラメータkeywordで部分一致検索をし、該当するグルメ情報をJSONで返します。キーワードが空の場合は全件を返します。

  • POST /details:選択グルメの詳細取得(新仕様)

リクエストボディで受け取ったJSONに対し、FetchDetailsで並列に詳細情報を取得し、結果を返します。JSONの形式が正しくない場合は400エラーを返します。

⑥ main.go - エントリーポイント

main.goは、Goアプリ全体のエントリーポイント(指令塔)です。Goアプリケーションはmain.goの中のmain()関数から実行され、Webサーバーの起動、ルーティングの設定、リポジトリの初期化など、すべての処理の起点をここで制御します。

main.go
package main

import (
	"osaka-gourmet-backend/gourmet"

	"github.com/gin-gonic/gin"
)

// main アプリケーションのエントリーポイント
func main() {
	r := gin.Default()

	// フロントエンドからの接続を許可(CORS設定)
	r.Use(func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204) // プリフライトリクエストは中身不要で204返す
			return
		}

		c.Next()
	})

	// リポジトリを初期化し、ルートを登録してサーバーを起動
	repo := gourmet.NewMemoryRepo()
	gourmet.RegisterRoutes(r, repo)
	r.Run(":8080")
}

フロントエンドからAPIを呼び出す際、CORS(クロスオリジンリソース共有)による制限が発生することがあります。これを回避するために、全オリジンからのアクセスを許可する設定を行っています(本番環境では信頼できるオリジンに制限することが推奨)。

gourmetパッケージに定義された関数で、メモリにグルメデータを初期化し、リポジトリインスタンスを作成しています。

ルーティング処理(APIエンドポイントの定義)はgourmetパッケージに切り出してあり、ここでGinに登録しています。この関数内で/gourmets/detailsなどのパスと、対応するハンドラー関数が関連付けられます。

Webサーバーを起動し、APIを確認します。

go run main.go


localhost:8080/gourments


localhost:8080/details

⑦ gourmet/model_test.go - ユニットテスト

Go言語におけるユニットテストの基本的な書き方についても、簡単に触れてみます。

gourmet/model_test.go
package gourmet

import "testing"

// TestRename Renameメソッドが正常に動作することをテストする
func TestRename(t *testing.T) {
	s := Gourmet{ID: 1, Name: "旧グルメ名"}
	s.Rename("新グルメ名")

	if s.Name != "新グルメ名" {
		t.Errorf("期待: 新グルメ名, 実際: %s", s.Name)
	}
}

import "testing" は Go 言語の標準テストパッケージであり、特別な設定をしなくてもgo testにより自動的にテストが実行されます。

外部ライブラリなしでテストが実行できるのは、Goの特徴ですね。

実際にテストを実行してみたところ、正常にパスしました。

***@MacBookPro backend % go test ./...

?       osaka-gourmet-backend             [no test files]
ok      osaka-gourmet-backend/gourmet     0.737s

⑧ Reactで /gourmets + /details を表示

ここまででバックエンド側の実装は完了したので、フロントエンドの React(TypeScript)側でGo APIと連携し、グルメ一覧を検索・表示する機能を構築します。通信の中心は/gourmetsエンドポイントです。

まずは、ReactアプリをViteを使ってセットアップします。

cd ../frontend
npm create vite@latest osaka-gourmet-frontend
# → Select a framework: React
# → Select a variant: TypeScript

cd osaka-gourmet-frontend
npm install

次に。GoのGourmet構造体に対応する型定義をTypeScript側で作成します。

src/types.ts
export type Gourmet = {
  id: number;
  name: string;
};

メインコンポーネントでは、Go APIと通信し、検索キーワードに応じてグルメ一覧を取得・表示し、詳細データも取得できるようにします。

App.tsx
import { useEffect, useState } from 'react';
import './App.css';
import type { Gourmet } from './types';

function App() {
  const [gourmets, setGourmets] = useState<Gourmet[]>([]);
  const [keyword, setKeyword] = useState('');
  const [loading, setLoading] = useState(false);
  const [details, setDetails] = useState<string[]>([]);
  const [loadingDetails, setLoadingDetails] = useState(false);

  const fetchDetails = () => {
    setLoadingDetails(true);
    fetch('http://localhost:8080/details', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(gourmets),
    })
      .then(res => res.json())
      .then(data => {
        setDetails(data || []);
      })
      .catch(error => {
        console.error('詳細取得エラー:', error);
        setDetails([]);
      })
      .finally(() => {
        setLoadingDetails(false);
      });
  };

  useEffect(() => {
    if (keyword.trim() === '') {
      setGourmets([]);
      return;
    }

    setLoading(true);
    fetch(`http://localhost:8080/gourmets?keyword=${keyword}`)
      .then(res => res.json())
      .then(data => {
        console.log('APIレスポンス:', data);
        setGourmets(data || []);
      })
      .catch(error => {
        console.error('APIエラー:', error);
        setGourmets([]);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [keyword]);

  return (
    <div className="app">
      <header className="header">
        <h1 className="title">🍜 大阪グルメ検索</h1>
        <p className="subtitle">名物グルメを見つけよう!</p>
      </header>
      
      <div className="search-container">
        <input
          type="text"
          className="search-input"
          placeholder="🔍 "
          value={keyword}
          onChange={(e) => setKeyword(e.target.value)}
        />
      </div>

      <div className="results-container">
        {loading ? (
          <div className="loading">検索中...</div>
        ) : keyword.trim() === '' ? (
          <div className="no-results">上の検索ボックスで大阪っぽいグルメを検索してみてください</div>
        ) : gourmets.length === 0 ? (
          <div className="no-results">{keyword}」に一致するお店が見つかりませんでした</div>
        ) : (
          <ul className="gourmet-list">
            {gourmets.map((gourmet, index) => (
              <li key={gourmet.id ?? index} className="gourmet-item">
                <h3 className="gourmet-name">
                  {gourmet.name ?? JSON.stringify(gourmet)}
                </h3>
              </li>
            ))}
          </ul>
        )}
      </div>
      {gourmets.length > 0 && (
        <div style={{ marginTop: '1rem' }}>
          <button onClick={fetchDetails}>🔍 グルメの詳細を見る</button>
        </div>
      )}

      {loadingDetails ? (
        <div className="loading">詳細を取得中...</div>
      ) : details.length > 0 && (
        <div className="details-container">
          <h2>📋 詳細一覧</h2>
          <ul>
            {details.map((detail, i) => (
              <li key={i}>{detail}</li>
            ))}
          </ul>
        </div>
      )}

    </div>
  );
}

export default App;

通信の流れ(React ↔ Go サーバー)は以下のとおりです。

  1. ユーザーが「焼き」などキーワードを入力
  2. Reactが/gourmets?keyword=焼きをGoサーバーにリクエスト
  3. Goがキーワードにマッチするグルメ名を検索
  4. グルメ一覧をJSONで返す
  5. Reactが一覧を表示

さらに、「グルメの詳細を見る」を押すと/detailsエンドポイントにPOSTリクエストを送り、選択されたグルメに対する詳細情報を取得・表示します。

動作確認をします。

npm run dev


初期表示画面


「焼き」で検索した結果が一覧表示


「グルメの詳細を見る」ボタンで詳細情報が取得・表示

おお〜!ちゃんと検索できました!

まとめ

アプリとしてはシンプルすぎるかもしれませんが、GoのAPI作成からReactとの連携まで一通り体験できたのは、とても良い学びになりました。やはり、実際にアプリを作りながら学ぶことで、包括的に、経験ベースで理解が深まりますね〜。

題材は大阪グルメでなくても構わないので、自分の興味あるデータに置き換えてみれば、Goの基礎を学びたい人にとって、ちょうど良い入門例になるかもしれません。

Goの理解をより深めて、実用的なアプリ開発やチーム開発にも活かせるようにしていきます!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion