0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go初心者がGo+ginでAPIサーバーを作る

Last updated at Posted at 2025-12-17

背景

別言語を久々に学ぶモチベーションがあったのと、サーバー開発の幅を広めたいことから始めました!
Go自体は存在を知っていましたが、なかなか取り組めず今になって始めました。
サーバー開発のスタンダードという認識もあったので、Goでの開発方法・フレームワークの利用などを中心に記事を進めていきたいと思います。

筆者の経歴も簡単に
  • エンジニア歴5年
  • 学生からプログラミング学習していました
  • メイン:Typescript
    • Web開発(React・Next)
    • フルスタックは最近
  • IoT関連も経験
    • SORACOMさんのサービスを使ったソリューションの構築

Goを始める上でとりあえず最初に見た

Go Tourというチュートリアルサイトがあり、変数からArray,Slice,Struct....と順番に学んでいけます。
Goの簡単な構文理解にとても役立ちましたので、もしこれからGoのSyntaxを学びたい方は一見いただければと思います!

特にArray、Slice、Struct関連はかなりわかりやすく解説されていました。

構築するサービス

今回は一言日記を登録できるサービスを構築していきます!

サービスの流れとしては

  • ログイン
  • 今日の質問テーマが3つほど表示される
  • テーマを選んでそれに回答

また、過去の投稿を見るページも用意して、いつに、どんな質問に対して、どんな回答をしたかを確認できるようにもします。

StitchにWebページデザインも考えてもらったので、このイメージで作っていこうかなと思います!

image.png

image.png

ちなみに今回の記事では、フロントの構築は対象としないのでバッサリとカットしていきます。

環境構築

こちらの記事を参考にbrewでGoをインストール

合わせてVSCodeのGo拡張機能もインストールします。

後から気づいたのですが、goenvというgoのバージョン管理ツールもありました。
nodenvやpyenvみたいな存在なので、本格的にGoの開発をする場合は、このツール経由でインストールする方が良さそうですね。

この記事の進め方

この記事では

  • Goのセットアップ
  • サーバーの構築・モックAPIの作成
  • DBの接続・マイグレーション
  • APIロジックの作成・ORMによるDB操作
    となります。

記事がかなり長くなってしまったので、認証やデプロイメントは別記事にて書いていきたいと思います。
別記事のリンクはページ下部に掲載予定です!

プロジェクトの構築

Goプロジェクトを作成します。
まずはパッケージ管理を初期化しましょう。

go mod init daily-note-app

これでgo.modが作成されました。

今回はAPIサーバーをGinというフレームワークで構築していきます。

公式ドキュメント:https://gin-gonic.com/ja/

GinはExpress.js風ルーティングのシンプルさとGoの高性能を組み合わせ、次の用途に理想的です:

  • 高スループットなREST API構築
  • 多数の同時リクエストを捌くマイクロサービス開発
  • 高速レスポンスが要求されるウェブアプリ
  • 最小限のボイラープレートで素早くサービスを試作

NodeJSを使った開発に慣れている身としてExpressライクは助かる。
ということでこのフレームワークを選択しました。

フレームワークのインストールは下記のコマンドにて。

go get -u github.com/gin-gonic/gin

-uオプションって何??
取得しているものを更新してくれるみたいな理解。取得してなかったら最新のものを取得してくれる。

簡単なサーバーを作ってみる

公式ドキュメントのクイックスタートから、簡単なAPIサーバーを構築して動きを見てみましょう。
https://gin-gonic.com/ja/docs/

main.go
package main

import (
  "net/http"

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

func main() {
  // loggerとrecoveryミドルウェア付きGinルーター作成
  r := gin.Default()

  // 簡単なGETエンドポイント定義
  r.GET("/ping", func(c *gin.Context) {
    // JSONレスポンスを返す
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })

  // ポート8080でサーバー起動(デフォルト)
  // 0.0.0.0:8080(Windowsではlocalhost:8080)で待機
  r.Run()
}

このファイルをgo run main.goで実行して、http://localhost:8080/pingにアクセスするとpongというメッセージが返ってきました。

mockAPI を作ってみる

クライアント(今回はReactを利用)と通信するためのAPIを作ります。
まずは単純に指定されたJSON形式のデータを返すモックAPIを定義して、クライアントがある程度できたらDBの作成などをしていこうかなと思います。

モックAPIは先のドキュメントも参考にしつつ、下記のような形で定義してみました。

main.go
package main

import (
	"net/http"
	"time"

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

type question struct {
	Id int `json:"id"`
	QText string `json:"qtext"`
}

type daiary struct {
	Id int `json:"id"`
	Note string `json:"note"`
	UserId int `json:"userId"`
	Question question `json:"question"`
	CreatedAt time.Time `json:"createdAt"`
}

var mockQuestions = []question{
	{Id: 1, QText: "今の気分は?"},
	{Id: 2, QText: "今日の夕食は?"},
	{Id: 3, QText: "〇〇でいい感じ!"},
}

var mockDiaries = []daiary{
	{Id: 1, Note: "最高!", UserId: 1, Question: mockQuestions[0], CreatedAt: time.Now()},
	{Id: 2, Note: "ぼちぼち", UserId: 1, Question: mockQuestions[0], CreatedAt: time.Now()},
}

func main() {
	// loggerとrecoveryミドルウェア付きGinルーター作成
	r := gin.Default()

	{
		v1 := r.Group("/v1")

		v1.GET("/api/questions", func(c *gin.Context) {
			c.JSON(http.StatusOK, mockQuestions)
		})
		v1.POST("/api/questions", func(c *gin.Context) {
			c.String(http.StatusAccepted, `sended`);
		})
		

		v1.GET("/api/diaries", func(c *gin.Context) {
			c.JSON(http.StatusOK, mockDiaries)
		})
		v1.POST("/api/diaries", func(c *gin.Context) {
			c.String(http.StatusAccepted, `sended`);
		})
	}

	// ポート8080でサーバー起動(デフォルト)
	// 0.0.0.0:8080(Windowsではlocalhost:8080)で待機
	err := r.Run()
	if err != nil {
		return
	}
}

フロントエンドはReactでサクッと構築していきます。
(今回はGoサーバーがメインなので省略)

GoサーバーのモックAPIと通信して、下記のようにサンプルデータが取得できることを確認しました。
フロントエンドのサンプル画面

ここで1日目Done!

APIを作っていく!

モックAPIができたので、モデルの型やリクエスト・レスポンスJSONデータタイプが見えてきました!

今回は簡単にこんなイメージでDBを構築していきたいと思います。
dialy-note-model.png

せっかくなのでDB接続もORMを用いてやりたい!
今回は、GitHubのStar数や直近でメンテナンスもされており、公式ドキュメントもある(日本語も!)ことからGORMというORMを選択してみました。

とりあえずパッケージをインストール、今回はpostgresを使いたいのでpostgres向けのドライバーも取得しました。

$ go get -u gorm.io/gorm
$ go get -u gorm.io/driver/postgres

あわせてここで、つかうデータベースも構築しておきます。
psqlでpostgres操作コンソールにアクセスして、下記コマンドでデータベースを作成しました。
この名前diarynoteは、次に記述する.envファイルのDB_NAMEの値として記述しています。

# CREATE DATABASE diarynote;

接続に向けての準備

.envからDB接続情報を読ませるために下記のパッケージも利用してみました。

環境変数の値を取得する方法で参考にしたページはこちら。

公式ドキュメントも参考に、PostgresDBへ接続するコードを書いてみました。

src/database/gorm.go
package database

import (
	"fmt"
	"os"
	"strconv"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func ConnectPostgres() *gorm.DB {
	DB_HOST := os.Getenv("DB_HOST")
	DB_USER := os.Getenv("DB_USER")
	DB_NAME := os.Getenv("DB_NAME")
	DB_PORT := os.Getenv("DB_PORT")
	DB_PORTNUM, _ := strconv.Atoi(DB_PORT)

	dsn := fmt.Sprintf("host=%s user=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Tokyo", 
		DB_HOST, DB_USER, DB_NAME, DB_PORTNUM)
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if (err != nil) {
		fmt.Println(err.Error())
	}

	return db
}

このファイルを実行する前に、.envファイルを定義して、DB接続用の設定を追加しています。
設定した値は、DB_HOST,DB_USER,DB_NAME,DB_PORTです。

環境によってはDBユーザーのパスワードも必要なので設定してください。

モデルを定義してみる

公式ドキュメントに沿ってモデルも定義してみます。

今回はDBスキーマを先に定義しているので、そのうちの一つ、questionsテーブル・モデルを定義していきたいと思います。

このスキーマは外部キーもないので、とっかかりにはちょうど良さそうなため選定しました。

ちなみに、このモデルは別ファイルに定義して、さまざまな場所(DBのマイグレーションやレスポンスの型定義など)で再利用したいと思います。

src/model/question.go
package model

import (
	"time"

	"gorm.io/gorm"
)

type Question struct {
	gorm.Model
	ID uint `json:"id" gorm:"primaryKey;unique;autoIncrement"`
	QText string `json:"qtext"`
	CreatedAt time.Time `json:"createdAt"`
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

モデル=スキーマの定義ができたので、これをもとにマイグレーションしていきます。
databaseの接続を行ったConnectPostgres関数にマイグレーションのコードも追加します。

ちなみにローカルの別パッケージ(ファイル)の取り込み方はこちらを参照しました!

src/database/gorm.go
import {
    // ...略
	"github.com/lunasky-hy/dialy-note-app/src/model"
}

func ConnectPostgres() *gorm.DB {
    // ...略
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if (err != nil) {
		fmt.Println(err.Error())
	}

	db.AutoMigrate(&model.Question{}) // <---- ここを追加
	return db
}

この関数をmain.goで実行してみると無事マイグレートまでできました!

リポジトリを作成してみる(レイヤードアーキテクチャその1)

Typescript開発者目線での衝撃ポイント:Goではクラスが使えない....

JS/TSなら下記のようなイメージでDBアクセス用のリポジトリオブジェクトを作成していました。

class Repository {
    constructor(db) {...}
    select() { db.create(); ... }
    insert(obj) { db.create(); ... }
}

どうやってGORMのDBインスタンスを渡して、insertやselectなどのメソッドをリポジトリ内で定義しようかと悩み....

ここで、Structの勉強をもう一度やり直して、メソッドの定義の仕方も復習しました。
structにメソッドを定義できることがわかり、最高にクールに解決しました!

ちなみに構造体とメソッドの関係、使い方についてはこちらを参考にさせていただきました。

src/repository/diarynote.go
package repository

import (
	"context"

	"github.com/lunasky-hy/dialy-note-app/src/model"
	"gorm.io/gorm"
)

type DiaryRepository struct {
	db *gorm.DB
	ctx context.Context
}

func (d DiaryRepository) CreateQuestion(question model.Question) error {
	err := gorm.G[model.Question](d.db).Create(d.ctx, &question)
	return err
}

func (d DiaryRepository) ReadQuestion() ([]model.Question, error) {
	questions, err := gorm.G[model.Question](d.db).Find(d.ctx)
	return questions, err
}

func CreateRepository(db *gorm.DB) DiaryRepository {
	context := context.Background()
	repos := DiaryRepository{db, context}
	return repos
}

リポジトリができたので依存注入して、早速APIに組み込んでみましょう!

想定する流れとしては、

  1. main.goに定義しているDBインスタンスを使って、リポジトリインスタンスを作成します
  2. そのリポジトリを使って、QuestionsAPIのGET/POSTを定義していきます

リポジトリを使った処理を下記のように記述してみました!

main.go
import {}
func main() {
    // ...

	db := database.ConnectPostgres() // <--- DBインスタンスの作成
	repos := repository.CreateRepository(db) // <--- リポジトリインスタンスの作成

	// loggerとrecoveryミドルウェア付きGinルーター作成
	r := gin.Default()

	{
		v1 := r.Group("/v1")

		v1.GET("/api/questions", func(c *gin.Context) {
			ques, _ := repos.ReadQuestion() // <----リポジトリのReadを使って読み出し
			c.JSON(http.StatusOK, ques)
		})
		v1.POST("/api/questions", func(c *gin.Context) {
			var json model.Question
			if err := c.ShouldBindJSON(&json); err != nil {
				return
			}
			fmt.Println(json.QText);
			repos.CreateQuestion(json); // <----リポジトリのCreateを使って登録処理
			c.String(http.StatusAccepted, `sended`);
		})

これでmain.goを実行してみます。
フロント側で軽く操作してみると...無事動きました!!

フロントでGETしてデータが表示できている様子と、サーバー側のログも!

スクリーンショット 2025-12-09 16.29.14.png

スクリーンショット 2025-12-09 16.29.53.png

コントローラーを作成してみる(レイヤードアーキテクチャその2)

ちなみに、レイヤードアーキテクチャ(もどき)をやってみていますが、これは責任の分離と再利用性・保守性を目指したもので、今回のGoに限らず、さまざまなシステムで採用されている構造です。
詳細については、先輩方の記事が参考になりますのでよければご一読ください。

今回は簡単にできるレイヤードという感じで下記のような構成を想定しています。
API ←→ 各コントローラー ←→ リポジトリ ←→ GORM ←→ DB

名称未設定ファイル.drawio.png

すでに、ORM・リポジトリ・APIは作成しているので、残るコントローラーを作成します!

コントローラーに当たる部分は、main.goの部分ですでに定義していました。
今回はそれを別ファイルにて定義していきます。
具体的には、リポジトリの構築を参考にし、Structを使って、メソッドを定義していくような感じです。

src/controller/questions.go
package controller

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/lunasky-hy/dialy-note-app/src/model"
	"github.com/lunasky-hy/dialy-note-app/src/repository"
)

type QuestionController struct {
	repos repository.DiaryRepository
}

// main.goのv1.GET("/api/questions",...の中の 「func(c *gin.Context)」と一緒
func (qc QuestionController) Get(c *gin.Context) {
	ques, _ := qc.repos.ReadQuestion()
	c.JSON(http.StatusOK, ques)
}

// main.goのv1.POST("/api/questions",...の中の 「func(c *gin.Context)」と一緒
func (qc QuestionController) Post(c *gin.Context) {
	var json model.Question
	if err := c.ShouldBindJSON(&json); err != nil {
		return
	}
	fmt.Println(json.QText);
	qc.repos.CreateQuestion(json);
	c.String(http.StatusAccepted, `sended`);
}

func CreateQuestionController(repos repository.DiaryRepository) QuestionController {
	controller := QuestionController{repos: repos}

	return controller
}

そして、main.goでは次のようにルーティングを定義します。

main.go
func main() {
	db := database.ConnectPostgres()
	repos := repository.CreateRepository(db)
	questionController := controller.CreateQuestionController(repos)

	// loggerとrecoveryミドルウェア付きGinルーター作成
	r := gin.Default()

	{
		v1 := r.Group("/v1")

		v1.GET("/api/questions", questionController.Get)
		v1.POST("/api/questions", questionController.Post)
    // ...略
        

ここで2日目終了!

他のエンドポイント・テーブルも同様に作成していく!

他に必要なエンドポイントも順に追加していきます。

上ですでに記載しているリレーション図を再掲しますが、次はdiariesテーブルにアクセスするAPIを作っていきたいと思います。
このテーブルはquestionsテーブルとのリレーションがあり、GORMでのモデル定義でも外部参照を考える必要があります。
dialy-note-model.png

公式ドキュメントのBelong ToHas Manyを参考に外部参照をモデルで定義してみました!

src/model/diary.go
type Diary struct {
	gorm.Model
	ID uint `json:"id" gorm:"primaryKey;unique;autoIncrement"`
	Note string
	UserID uint
	QuestionID uint
	Question Question `gorm:"foreignKey:QuestionID"`
	CreatedAt time.Time `json:"createdAt"`
	DeletedAt gorm.DeletedAt `gorm:"index"`
}
src/model/question.go
type Question struct {
	gorm.Model
    // ...略
	Diaries []Diary  //<---追加
}

サービスレイヤーも追加してみる!(レイヤードアーキテクチャ3)

ここで気づいたのですが、サービスレイヤー:ビジネスロジックを受け持つ層も必要だと考えました。

主な役割は、

  • データベースの登録時に、必要な変換処理を行う
  • 外部キー制約があるデータについて、存在確認を行う
  • データベースを操作する前に、正しいユーザーが取り扱い対象のデータの操作(UPDATE、DELETE等)をしているかの判定

といった部分です。
要は、コントローラー(APIのリクエスト受け取り・レスポンス作成)とリポジトリ(DBの操作)の間に入って、受け取ったデータに必要な処理をしてDBへ渡す役割となります。

今回は簡単にできるレイヤードという感じで下記のような構成を想定しています。
API ←→ 各コントローラー ←→ リポジトリ ←→ GORM ←→ DB

上記で記載していたこの構成にサービスレイヤーを追加します。
API ←→ 各コントローラー ←→ 各サービス ←→ リポジトリ ←→ GORM ←→ DB

このサービスレイヤーの構築は、これまでのコントローラーの処理を抜き出して記載するだけなのでパパッと終えました!

src/service/diaries.go
type DiaryService struct {
	repos repository.DiaryRepository
}

func (s DiaryService) Find() ([]model.Diary, error) {
	diaries, error := s.repos.DiariesFind(1)
    // 上でUserID: 1に指定しているのは、まだ認証やユーザー登録機能を追加していないため、サンプルユーザーとして設定しています。
	return diaries, error
}

func (s DiaryService) Create(d model.Diary) (error) {
	newData := model.Diary{UserID: 1, Note: d.Note, QuestionID: d.QuestionID}
    // 上でUserID: 1に指定しているのもFindに記載したものと同様
	error := s.repos.DiaryCreate(newData)
	return error
}

func CreateDiaryService(repos repository.DiaryRepository) DiaryService {
	s := DiaryService{repos: repos}
	return s
}

APIエンドポイント /api/Diariesを処理するコントローラーはこちら。

src/controller/diaries.go
type DiariesController struct {
	service service.DiaryService
}

func (qc DiariesController) Get(c *gin.Context) {
	ques, _ := qc.service.Find()
	c.JSON(http.StatusOK, ques)
}

func (qc DiariesController) Post(c *gin.Context) {
	var json model.Diary
	if err := c.ShouldBindJSON(&json); err != nil {
		return
	}
	qc.service.Create(json);
	c.String(http.StatusAccepted, `sended`);
}

func CreateDiaryController(service service.DiaryService) DiariesController {
	controller := DiariesController{service: service}
	return controller
}

データベースアクセス用リポジトリは、既存のStructにメソッドを追加する形で機能追加しました。

src/repository/diarynote.go

func (d DiaryRepository) DiaryCreate(diary model.Diary) error {
	err := gorm.G[model.Diary](d.db).Create(d.ctx, &diary)
	return err
}

func (d DiaryRepository) DiariesFind(userId uint) ([]model.Diary, error) {
	var diaries []model.Diary
	error := d.db.Model(&model.Diary{}).Preload("Question").Where(&model.Diary{UserID: userId}).Find(&diaries).Error
	return diaries, error
}

func CreateRepository(db *gorm.DB) DiaryRepository { /* 略 */ }

このリポジトリではDiaryテーブルの抽出をWhereメソッドで行いつつ、Questionテーブルの情報も紐づけて抽出しています。

実際のレスポンスでは、question.qtextという、Diaryテーブルから見て別テーブル(Questionテーブル)のデータが格納されているのが確認できます。

スクリーンショット 2025-12-10 12.04.36.png

小噺:Findでハマったところ このFindの式で1時間ほどハマりました泣

原因は.Find(&diaries)とすべきところを.Find(diaries)としていたことで、これによりアクセスエラーになっていたようです。

これ、C言語とかでもよくある参照渡し・アドレス渡しですが、Goでもきちんと意識しないといけないなと感じました。

GETもPOSTもできるようになったことで、フロントエンドでも表示ができるようになりました!
スクリーンショット 2025-12-10 12.19.41.png

ここまでのまとめ

ここまでの構築で使えるようになった機能は次の通りです。

  • APIエンドポイントの作成
    • GET/v1/api/questions
    • POST/v1/api/questions
    • GET/v1/api/diaries
    • POST/v1/api/diaries
  • DBのスキーマモデルの定義
  • DBのマイグレーション
  • QuestionsテーブルのRead、Create操作
  • DiariesテーブルのRead、Create操作

サービス利用者目線でのできること(フロントエンド側)

  • 表示したい質問の登録
  • 質問の表示(複数・個数制限なし)
  • 質問の回答の登録
  • これまでの一言日記の表示

この記事を書く上で構築しているプロジェクトは下記となります。

自分が感じたGoとJavascript/Nodeとの違い

  • GoはJavaのようなpackage単位でアクセス可能
    • JavascriptはファイルごとにExportするものを選び、ファイルを選択してインポートする
  • Goはmainパッケージが必要、main.goで実行する
    • Javascript・Nodeはパッケージの指定なしで実行可能(エントリポイントであるindex.jsなどは必要)
  • GoはPublicなメソッドを大文字から命名し、小文字からのメソッドはPrivateとなる
    • Javascriptはexport句で指定する
  • GoはStructで複数の値を取り扱うオブジェクト的な利用が可能(クラス・オブジェクトという概念がない)
    • Javascriptはキーバリューペアで自由に定義可能
  • GoのSyntaxは変数名 型という順であり、括弧の省略が多い(IF,FOR文やfunc宣言など)
    • Typescriptも同様の順であるが、temp: stringというように「:」で繋ぐことから、見た目上は大きく違うように見える

個人的に難しいと感じたところ

  • packageの理解
    • おそらくJavaライクな感じ?
    • Javascript・Typescript開発メインだと、ファイルごとにインポートしていた
    • Goではディレクトリがとても意味を持つ
  • Publicなメソッド・値の理解
    • 先頭文字を大文字にするとPublicになるという簡易さ
    • Javascript・Typescriptではexport句で指定する必要ありexport function foge()、クラスメソッドは基本public
  • Structの理解
    • Cのような値だけを入れる構造体かと思ったらメソッドも定義可能
    • Javascript・Typescriptのclassのような使い方もできるが、メソッドの定義の仕方が特殊
  • Array・Sliceの理解
    • ArrayとSliceでは固定長・可変長という違いかと思いきや全く違う存在
    • Arrayは実際に複数の値が入っているオブジェクトで、Sliceはそれを切り出してみることができるアクセサ的な存在
    • Sliceは可変なので長さを変えられる。が、「容量」という概念があり、あるスライスの容量を超えるようなスライス操作をすると、別のオブジェクトとして生成される。
    • 今回の構築ではあまり意識する必要はなかったが、気をつける必要あり
  • ポインタの理解
    • もともとCは経験していたので、アドレスわたしなどは理解しているものの、スクリプト言語で意識しないのでその点は注意が必要
    • アドレス計算など複雑な操作はないとGoTourに書かれていたのでそこは安心

思ったこと

Goでのサーバー構築は、言語概念・SyntaxがJavascript・Typescriptと大きく違うところからハードルの高さも感じました。
ただ、プロジェクトを進めていく中でドキュメントが充実しているなという印象があります。
GoTourだったり、GORMのドキュメントやGinのドキュメントはメンテナンスされていて、詰まった時も進めやすさはすごく感じました。

まとめと次回の予告

Go初心者がサーバー開発を行うプロジェクト、ここまでお読みいただきありがとうございました。

あまりQiita慣れもしていないので読みにくい記事で申し訳ございません。
また、設計などもまだまだ経験が浅いところもあるので、アドバイスなどありましたらぜひいただきたく思います。

次回は「認証を「雑に」実装してみる」です。

こちらもよければご一読ください!

お疲れさまでした!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?