1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. WebFrameworks
  5. Gorilla - Golang learning step 5-4

Gorilla - Golang learning step 5-4

  • 公開日
  • カテゴリ:WebFrameworks
  • タグ:Golang,roadmap.sh,学習メモ
Gorilla - Golang learning step 5-4

roadmap.sh > Go > Web Frameworks > Gorilla の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. 参考 URL
  3. Gorilla
    1. 事前準備
  4. Gorilla の主要なパッケージと用途
    1. 1. gorilla/mux: 高機能なルーター
  5. 2. gorilla/handlers: HTTP リクエスト/レスポンスのためのミドルウェア
  6. 3. gorilla/sessions: セッション管理のためのパッケージ
    1. セッションのオプション設定
    2. ファイルベースのセッション
  7. 4. gorilla/websocket: WebSocket 通信のためのパッケージ

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

参考 URL

Gorilla

Gorilla は、Go 言語での Web アプリケーション開発を簡単にするための軽量なフレームワーク (FW) です。Gorilla は「フルスタックのフレームワーク」ではなく、必要な機能ごとに独立したパッケージを提供しているのが特徴です。そのため、必要な機能だけを選んで使うことができます。

事前準備

プロジェクトを作成しておきます。

mkdir myapp
cd myapp
go mod init myapp

Gorilla の主要なパッケージと用途

1. gorilla/mux: 高機能なルーター

gorilla/mux は、Go の標準ライブラリの net/http パッケージを拡張したルーターで、以下のような機能を提供します。

  • 動的なルーティング: URL パラメーター (/user/{id}) をサポート
  • メソッド制限: HTTP メソッド(GET, POST など)に基づくルーティング
  • リクエストのホスト名やスキームに基づくルーティング
  • クエリパラメーターのマッチング

インストール

go get -u github.com/gorilla/mux

実装例

gorilla/mux を使った簡単なルーティング実装例です。

package main

import (
  "fmt"
  "net/http"
  "github.com/gorilla/mux"
)

func main() {
  // 新しいルーターを作成
  r := mux.NewRouter()
  
  // ルートの定義
  r.HandleFunc("/", HomeHandler).Methods("GET")
  r.HandleFunc("/users/{id:[0-9]+}", UserHandler).Methods("GET")
  
  // サーバーの起動
  http.ListenAndServe(":8080", r)
}

func HomeHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, World!")
}

func UserHandler(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  userId := vars["id"]
  fmt.Fprintf(w, "User ID: %s", userId)
}

以下のコマンドでサーバーを起動します。

go run main.go

ブラウザで http://localhost:8080 にアクセスすると、「Hello, World!」が表示されます。

ブラウザで http://localhost:8080/users/123 にアクセスすると、「User ID: 123」が表示されます。

2. gorilla/handlers: HTTP リクエスト/レスポンスのためのミドルウェア

gorilla/handlers は、HTTP リクエスト/レスポンスを処理するための便利なミドルウェアを提供します。一般的な Web アプリケーションで必要となる以下のような機能を実装できます。

  • ロギング: HTTP リクエストのログ出力
  • CORS: クロスオリジンリソース共有の設定
  • 圧縮: レスポンスの自動圧縮
  • リカバリー: パニック発生時の処理

インストール

go get -u github.com/gorilla/handlers

実装例

gorilla/handlers を使った基本的なミドルウェア実装例です。

package main

import (
  "github.com/gorilla/handlers"
  "github.com/gorilla/mux"
  "net/http"
  "os"
)

func main() {
  // 新しいルーターを作成
  r := mux.NewRouter()

  // 基本的なルートの設定
  r.HandleFunc("/", HomeHandler).Methods("GET")

  // ミドルウェアの適用
  // 1. ログ出力
  loggedRouter := handlers.LoggingHandler(os.Stdout, r)

  // 2. CORS設定
  corsRouter := handlers.CORS(
    handlers.AllowedOrigins([]string{"http://localhost:3000"}),
    handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE"}),
    handlers.AllowedHeaders([]string{"Content-Type", "Authorization"}),
  )(loggedRouter)

  // 3. レスポンス圧縮
  compressedRouter := handlers.CompressHandler(corsRouter)

  // 4. パニックリカバリー
  finalRouter := handlers.RecoveryHandler()(compressedRouter)

  // サーバーの起動
  http.ListenAndServe(":8080", finalRouter)
}

func HomeHandler(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Welcome to the home page!"))
}

以下のコマンドでサーバーを起動します。

go run main.go

このサーバーは以下の機能を備えています。

  • すべてのリクエストがコンソールにログ出力されます
  • 指定したオリジンからのクロスオリジンリクエストを許可します
  • レスポンスが自動的に圧縮されます
  • パニックが発生してもサーバーがクラッシュしません

ブラウザで http://localhost:8080 にアクセスすると、「Welcome to the home page!」が表示され、同時にコンソールにリクエストログが出力されます。

::1 - - [05/Dec/2024:23:33:54 +0900] "GET / HTTP/1.1" 200 25

各ミドルウェアは必要に応じて個別に使用することもできます:

// ロギングだけ使用する場合
http.ListenAndServe(":8080", handlers.LoggingHandler(os.Stdout, r))

// CORSだけ使用する場合
http.ListenAndServe(":8080", handlers.CORS()(r))

// 圧縮だけ使用する場合
http.ListenAndServe(":8080", handlers.CompressHandler(r))

3. gorilla/sessions: セッション管理のためのパッケージ

gorilla/sessions は、Cookieベースのセッション管理を提供するパッケージです。以下のような機能を備えています。

  • 複数のセッションタイプ: Cookie または FileStore でのセッション管理
  • セッションの暗号化
  • フラッシュメッセージ
  • 有効期限の設定

インストール

go get -u github.com/gorilla/sessions

実装例

gorilla/sessions を使った基本的なセッション管理の実装例です。

package main

import (
  "fmt"
  "github.com/gorilla/sessions"
  "github.com/gorilla/mux"
  "net/http"
)

// セッションストアの作成(安全な鍵管理を推奨)
var store = sessions.NewCookieStore([]byte("secret-key-32-bytes-long-password"))
// 注意: 上記の鍵はサンプルのための簡易例です。
// 本番環境では、以下のように環境変数や AWS Secrets Manager のような
// 秘密管理サービスを使用して安全に管理してください。
//
// 例:
// var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
//
// 環境変数の設定例(.env ファイル):
// SESSION_SECRET=your-strong-secret-key

func main() {
  r := mux.NewRouter()

  // ルートの定義
  r.HandleFunc("/login", LoginHandler).Methods("GET")
  r.HandleFunc("/profile", ProfileHandler).Methods("GET")
  r.HandleFunc("/logout", LogoutHandler).Methods("GET")

  http.ListenAndServe(":8080", r)
}

// ログイン処理
func LoginHandler(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "session-name")
  
  // セッションへの値の設定
  session.Values["authenticated"] = true
  session.Values["user"] = "John Doe"
  
  // セッションの保存
  session.Save(r, w)
  
  fmt.Fprintln(w, "You have been logged in!")
}

// プロフィール表示(セッションチェック付き)
func ProfileHandler(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "session-name")

  // 認証チェック
  auth, ok := session.Values["authenticated"].(bool)
  if !ok || !auth {
    http.Error(w, "Please log in first", http.StatusUnauthorized)
    return
  }

  // ユーザー名の取得
  username := session.Values["user"].(string)
  fmt.Fprintf(w, "Welcome, %s!", username)
}

// ログアウト処理
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "session-name")
  
  // セッション値のクリア
  session.Values["authenticated"] = false
  delete(session.Values, "user")
  
  session.Save(r, w)
  
  fmt.Fprintln(w, "You have been logged out!")
}
go run main.go

このサーバーは以下の機能を備えています。

  • /login にアクセスするとセッションが作成されます
  • /profile では認証済みユーザーのみ情報を表示します
  • /logout でセッションがクリアされます

セッションのオプション設定

セッションの動作をカスタマイズすることもできます。

func main() {
  store := sessions.NewCookieStore([]byte("secret-key"))

  // セッションの設定
  store.Options = &sessions.Options{
    Path:   "/",           // Cookieのパス
    MaxAge:   86400,           // 有効期限(秒)
    HttpOnly: true,          // JavaScriptからのアクセスを防止
    Secure:   true,          // HTTPS接続のみ
    SameSite: http.SameSiteStrictMode, // SameSite属性
  }
}

ファイルベースのセッション

メモリ使用量を抑えるために、ファイルベースのセッションストアを使用することもできます。

package main

import (
  "github.com/gorilla/sessions"
  "encoding/gob"
)

// カスタム型をセッションに保存する場合は登録が必要
func init() {
  gob.Register(&User{})  // カスタム構造体を登録
}

var store = sessions.NewFilesystemStore("./sessions", []byte("secret-key"))

type User struct {
  ID  int
  Name  string
  Email string
}

gorilla/sessions は、セッション管理の基本機能を簡単に実装できる一方で、セキュアなアプリケーションを作るためには、開発者自身が適切なセキュリティ設定を行う必要があります。

4. gorilla/websocket: WebSocket 通信のためのパッケージ

gorilla/websocket は、WebSocket プロトコルを実装したパッケージです。リアルタイム通信を実現するために以下のような機能を提供します。

  • WebSocket サーバーの実装
  • クライアントとの双方向通信
  • メッセージの送受信
  • 接続管理

インストール

go get -u github.com/gorilla/websocket

実装例

gorilla/websocket を使った基本的なチャットサーバーの実装例です。

ディレクトリ・ファイル構造

myapp
├── chat-server
│   └── main.go
└── chat-client
  └── main.go

chat-server/main.go:

package main

import (
  "github.com/gorilla/mux"
  "github.com/gorilla/websocket"
  "log"
  "net/http"
  "sync"
)

var upgrader = websocket.Upgrader{
  ReadBufferSize:  1024,
  WriteBufferSize: 1024,
  CheckOrigin: func(r *http.Request) bool {
    return true
  },
}

type Client struct {
  conn   *websocket.Conn
  send   chan []byte
  username string
}

type Hub struct {
  clients  map[*Client]bool
  broadcast  chan []byte
  register   chan *Client
  unregister chan *Client
  mutex    sync.Mutex
}

func newHub() *Hub {
  return &Hub{
    clients:  make(map[*Client]bool),
    broadcast:  make(chan []byte),
    register:   make(chan *Client),
    unregister: make(chan *Client),
  }
}

func (h *Hub) run() {
  for {
    select {
    case client := <-h.register:
      h.mutex.Lock()
      h.clients[client] = true
      message := []byte("System: " + client.username + " joined the chat")
      for c := range h.clients {
        select {
        case c.send <- message:
        default:
          close(c.send)
          delete(h.clients, c)
        }
      }
      h.mutex.Unlock()

    case client := <-h.unregister:
      h.mutex.Lock()
      if _, ok := h.clients[client]; ok {
        delete(h.clients, client)
        close(client.send)
        message := []byte("System: " + client.username + " left the chat")
        for c := range h.clients {
          select {
          case c.send <- message:
          default:
            close(c.send)
            delete(h.clients, c)
          }
        }
      }
      h.mutex.Unlock()

    case message := <-h.broadcast:
      h.mutex.Lock()
      for client := range h.clients {
        select {
        case client.send <- message:
        default:
          close(client.send)
          delete(h.clients, client)
        }
      }
      h.mutex.Unlock()
    }
  }
}

func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    return
  }

  username := r.URL.Query().Get("username")
  if username == "" {
    username = "Anonymous"
  }

  client := &Client{
    conn:   conn,
    send:   make(chan []byte, 256),
    username: username,
  }

  hub.register <- client

  go func() {
    defer func() {
      hub.unregister <- client
      conn.Close()
    }()

    for {
      _, message, err := conn.ReadMessage()
      if err != nil {
        break
      }
      formattedMessage := []byte(client.username + ": " + string(message))
      hub.broadcast <- formattedMessage
    }
  }()

  go func() {
    defer conn.Close()
    for {
      select {
      case message, ok := <-client.send:
        if !ok {
          conn.WriteMessage(websocket.CloseMessage, []byte{})
          return
        }

        w, err := conn.NextWriter(websocket.TextMessage)
        if err != nil {
          return
        }
        w.Write(message)

        if err := w.Close(); err != nil {
          return
        }
      }
    }
  }()
}

func main() {
  hub := newHub()
  go hub.run()

  r := mux.NewRouter()
  r.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    handleWebSocket(hub, w, r)
  })

  log.Println("Server starting at :8080")
  http.ListenAndServe(":8080", r)
}

chat-client/main.go:

package main

import (
  "bufio"
  "flag"
  "fmt"
  "github.com/gorilla/websocket"
  "log"
  "os"
  "os/signal"
)

var addr = flag.String("addr", "localhost:8080", "server address")
var username = flag.String("username", "Anonymous", "username for chat")

func main() {
  flag.Parse()

  interrupt := make(chan os.Signal, 1)
  signal.Notify(interrupt, os.Interrupt)

  url := fmt.Sprintf("ws://%s/ws?username=%s", *addr, *username)
  conn, _, err := websocket.DefaultDialer.Dial(url, nil)
  if err != nil {
    log.Fatal("dial:", err)
  }
  defer conn.Close()

  done := make(chan struct{})

  go func() {
    defer close(done)
    for {
      _, message, err := conn.ReadMessage()
      if err != nil {
        log.Println("Error:", err)
        return
      }
      fmt.Printf("%s\n", message)
    }
  }()

  go func() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
      message := scanner.Text()
      err := conn.WriteMessage(websocket.TextMessage, []byte(message))
      if err != nil {
        log.Println("Error:", err)
        return
      }
    }
  }()

  for {
    select {
    case <-done:
      return
    case <-interrupt:
      err := conn.WriteMessage(
        websocket.CloseMessage,
        websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
      if err != nil {
        return
      }
      select {
      case <-done:
      }
      return
    }
  }
}

サーバーを起動します

cd chat-server
go run main.go

2 つのターミナルでそれぞれクライアントを起動します

ターミナル 1: User1

cd chat-client
go run main.go -username "User1"

ターミナル 2: User2

cd chat-client
go run main.go -username "User2"

それぞれで websocket を使用したチャットが動作します。

ターミナル 1: User1

% go run main.go -username "User1"
System: User1 joined the chat
System: User2 joined the chat
こんにちは!
User1: こんにちは!
User2: やあ、こんにちは!

ターミナル 2: User2

% go run main.go -username "User2"
System: User2 joined the chat
User1: こんにちは!
やあ、こんにちは!
User2: やあ、こんにちは!

まとめ

  • Gorilla は必要な機能ごとに独立したパッケージを提供する軽量なフレームワーク
  • gorilla/mux は高機能なルーティングを提供するパッケージ
  • gorilla/handlers は HTTP リクエストやレスポンスの処理を簡単にするミドルウェアを提供
  • gorilla/sessions は Cookie を利用したセッション管理を可能にするパッケージ
  • gorilla/websocket は WebSocket を利用したリアルタイム通信を実現するパッケージ
  • 各パッケージの活用により、安全かつ効率的な Web アプリケーションの構築が可能


[Next] Step 6-1: log/slog

[Prev] Step 5-3: Fiber

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級