diff --git a/README.md b/README.md index 0aaaa36..b0b06b7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Go言語の共通モジュールを作成する - ## Go言語の特徴 * シンプルで学習コストが低い構造 @@ -11,4 +10,10 @@ Go言語の共通モジュールを作成する * クロスコンパイルが容易 (Linux, Windows, macOS などへ簡単に出力可能) * 静的型付け * クラスを持たず、構造体とメソッドでオブジェクト指向的な書き方ができる -* 標準ライブラリが強力で小さなバイナリにまとまる \ No newline at end of file +* 標準ライブラリが強力で小さなバイナリにまとまる + +## Go言語を実行する + +```sh +go run src/hello.go +``` \ No newline at end of file diff --git a/examples/api_client.go b/examples/api_client.go new file mode 100644 index 0000000..78e6fb6 --- /dev/null +++ b/examples/api_client.go @@ -0,0 +1,188 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// APIClient はHTTPリクエストを送信するクライアント +type APIClient struct { + BaseURL string + HTTPClient *http.Client +} + +// NewAPIClient は新しいAPIクライアントを作成 +func NewAPIClient(baseURL string) *APIClient { + return &APIClient{ + BaseURL: baseURL, + HTTPClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// Get はGETリクエストを送信 +func (c *APIClient) Get(endpoint string) ([]byte, error) { + url := c.BaseURL + endpoint + + resp, err := c.HTTPClient.Get(url) + if err != nil { + return nil, fmt.Errorf("GETリクエスト失敗: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("レスポンス読み込み失敗: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("ステータスコード: %d, レスポンス: %s", resp.StatusCode, string(body)) + } + + return body, nil +} + +// Post はPOSTリクエストを送信 +func (c *APIClient) Post(endpoint string, data interface{}) ([]byte, error) { + url := c.BaseURL + endpoint + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("JSONエンコード失敗: %w", err) + } + + resp, err := c.HTTPClient.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("POSTリクエスト失敗: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("レスポンス読み込み失敗: %w", err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("ステータスコード: %d, レスポンス: %s", resp.StatusCode, string(body)) + } + + return body, nil +} + +// Put はPUTリクエストを送信 +func (c *APIClient) Put(endpoint string, data interface{}) ([]byte, error) { + url := c.BaseURL + endpoint + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("JSONエンコード失敗: %w", err) + } + + req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("リクエスト作成失敗: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("PUTリクエスト失敗: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("レスポンス読み込み失敗: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("ステータスコード: %d, レスポンス: %s", resp.StatusCode, string(body)) + } + + return body, nil +} + +// Delete はDELETEリクエストを送信 +func (c *APIClient) Delete(endpoint string) ([]byte, error) { + url := c.BaseURL + endpoint + + req, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return nil, fmt.Errorf("リクエスト作成失敗: %w", err) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("DELETEリクエスト失敗: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("レスポンス読み込み失敗: %w", err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + return nil, fmt.Errorf("ステータスコード: %d, レスポンス: %s", resp.StatusCode, string(body)) + } + + return body, nil +} + +// 使用例 +func main() { + // APIクライアントの作成 + client := NewAPIClient("https://jsonplaceholder.typicode.com") + + // GETリクエストの例 + fmt.Println("=== GETリクエスト ===") + getData, err := client.Get("/posts/1") + if err != nil { + fmt.Printf("エラー: %v\n", err) + } else { + fmt.Printf("レスポンス: %s\n\n", string(getData)) + } + + // POSTリクエストの例 + fmt.Println("=== POSTリクエスト ===") + postData := map[string]interface{}{ + "title": "新しい投稿", + "body": "これはテスト投稿です", + "userId": 1, + } + postResp, err := client.Post("/posts", postData) + if err != nil { + fmt.Printf("エラー: %v\n", err) + } else { + fmt.Printf("レスポンス: %s\n\n", string(postResp)) + } + + // PUTリクエストの例 + fmt.Println("=== PUTリクエスト ===") + putData := map[string]interface{}{ + "id": 1, + "title": "更新された投稿", + "body": "これは更新されたテスト投稿です", + "userId": 1, + } + putResp, err := client.Put("/posts/1", putData) + if err != nil { + fmt.Printf("エラー: %v\n", err) + } else { + fmt.Printf("レスポンス: %s\n\n", string(putResp)) + } + + // DELETEリクエストの例 + fmt.Println("=== DELETEリクエスト ===") + deleteResp, err := client.Delete("/posts/1") + if err != nil { + fmt.Printf("エラー: %v\n", err) + } else { + fmt.Printf("レスポンス: %s\n", string(deleteResp)) + } +} diff --git a/examples/hello.go b/examples/hello.go new file mode 100644 index 0000000..73a505e --- /dev/null +++ b/examples/hello.go @@ -0,0 +1,11 @@ +// src/hello.go +// A simple Go program that prints "Hello, World!" to the console. +// go run src/hello.go +package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +} + diff --git a/readme/coding-conventions.md b/readme/coding-conventions.md new file mode 100644 index 0000000..a358466 --- /dev/null +++ b/readme/coding-conventions.md @@ -0,0 +1,377 @@ +# Go コーディング規約 + +## 1. 一般的なガイドライン + +1. コードフォーマットは **必ず `gofmt`(または `goimports`)に従う**。手で整形しない。 +2. インデントは **タブ**(`\t`)を使用する(`gofmt`が自動で設定する)。 +3. 1行の長さは厳密な制限はないが、**できれば 100 文字前後**を目安にする。 +4. 1ファイルは 300〜400 行以内を目安とし、長くなりすぎる場合はファイル分割を検討する。 +5. パッケージ単位で責務を分割し、**小さく・単機能な関数**を心がける。 +6. エクスポート(大文字始まり)するものは最小限にし、原則として **パッケージ内で完結する API を設計**する。 + +## 2. コメント/ドキュメント + +Go では **godoc** 形式のコメントが重要です。 + +1. **パッケージコメント**: `package` 宣言の直前に、そのパッケージの説明を書く。 + + ```go + // Package user provides user management functionalities such as + // registration, authentication, and profile updates. + package user + ``` + +2. **エクスポートされた関数/メソッド**には、**シグネチャ名から始まるコメント**を書く。 + + ```go + // CreateUser creates a new user and stores it into the repository. + func CreateUser(name string, age int) (*User, error) { + // 実装 + } + ``` + +3. **エクスポートされた型やフィールド**にもコメントを書く。 + + ```go + // User represents a user entity in the system. + type User struct { + // ID is a unique identifier of the user. + ID int64 + + // Name is the display name of the user. + Name string + } + ``` + +4. 非エクスポート(小文字始まり)のものでも、複雑な処理や分岐には**行コメント**で意図や前提条件を書いておく。 + + ```go + // cache にヒットした場合は DB に問い合わせない + if v, ok := cache[id]; ok { + return v, nil + } + ``` + +## 3. 命名規則 + +Go は **CamelCase / mixedCaps** を使います(snake_case をあまり使わない)。 + +1. **パッケージ名**: + + * すべて小文字、`_` や数字を極力避ける。 + * 短く意味のある名前にする(`users` より `user` のように単数形が好まれがち)。 + + ```go + package user // 良い例 + ``` + +2. **変数・関数名(非エクスポート)**: 先頭小文字の `mixedCaps`。 + + ```go + func loadUser() (*User, error) { + userCount := 0 + _ = userCount + // ... + return nil, nil + } + ``` + +3. **エクスポート関数・構造体**: 先頭大文字の `MixedCaps`。 + + ```go + type UserService struct { + repo Repository + } + + func NewUserService(repo Repository) *UserService { + return &UserService{repo: repo} + } + ``` + +4. **定数**: + + * エクスポートする定数は `CamelCase`。 + * パッケージ内専用であれば先頭小文字。 + + ```go + const DefaultTimeout = 5 * time.Second + + const maxRetryCount = 3 + ``` + +5. **レシーバ名**: + + * 型名の1〜2文字の略を使う。 + * `UserService` → `us`、`Server` → `s` など。 + + ```go + func (s *Server) Start() error { + // ... + return nil + } + ``` + +## 4. インポート/パッケージ構成 + +1. インポートは **自動整形ツール(goimports)にまかせる**。 + +2. グループ順は以下が一般的: + + 1. 標準ライブラリ + 2. サードパーティ + 3. 自プロジェクト内パッケージ + + グループ間は空行で区切る。 + + ```go + import ( + "context" + "fmt" + "net/http" + + "github.com/julienschmidt/httprouter" + + "example.com/project/internal/user" + "example.com/project/pkg/logger" + ) + ``` + +3. **循環参照**が起きないようにパッケージを分割する。 + + * `cmd/`… エントリポイント(`main` パッケージ) + * `internal/`… アプリ内部でのみ使うパッケージ + * `pkg/`… 外部にも公開可能なパッケージ + +## 5. エラーハンドリング + +1. Go のエラーは **戻り値の `error`** で扱う。 + + ```go + user, err := repo.FindByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to find user: %w", err) + } + ``` + +2. エラーはできるだけ **早めに return** し、ネストを浅く保つ。 + + ```go + if err != nil { + return nil, err + } + // ここから成功時の処理 + ``` + +3. `panic` は **プログラミングミスなど「復旧不能」なケースのみ**で使用し、通常のエラー処理には使わない。 + +4. `errors.Is` / `errors.As` を使ってエラー種別を判定する。 + + ```go + if errors.Is(err, sql.ErrNoRows) { + // not found の扱い + } + ``` + +## 6. 複数ファイル間の分割時の書き方 + +### 6.1 同じパッケージ内での分割 + +Go では **同じディレクトリ配下の `.go` ファイルは同じ `package` 名**であれば、1つのパッケージとして扱われます。 + +```text +user/ + user.go + service.go + repository.go +``` + +すべてのファイルで `package user` と書きます。 + +```go +// user/user.go +package user + +type User struct { + ID int64 + Name string +} +``` + +```go +// user/service.go +package user + +// Service handles user-related usecases. +type Service struct { + repo Repository +} + +func NewService(repo Repository) *Service { + return &Service{repo: repo} +} +``` + +```go +// user/repository.go +package user + +type Repository interface { + FindByID(id int64) (*User, error) + Save(user *User) error +} +``` + +同一パッケージ内であれば、**インポートなしですべてのシンボルを参照**できます。 + +### 6.2 `main` パッケージと内部パッケージの分割 + +```text +cmd/app/main.go +internal/user/service.go +internal/user/repository.go +``` + +```go +// cmd/app/main.go +package main + +import ( + "log" + + "example.com/project/internal/user" +) + +func main() { + repo := user.NewInMemoryRepository() + svc := user.NewService(repo) + + if err := svc.Run(); err != nil { + log.Fatal(err) + } +} +``` + +```go +// internal/user/service.go +package user + +type Service struct { + repo Repository +} + +func NewService(repo Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) Run() error { + // 実装 + return nil +} +``` + +```go +// internal/user/repository.go +package user + +type Repository interface { + // ... +} + +type inMemoryRepository struct { + // ... +} + +func NewInMemoryRepository() Repository { + return &inMemoryRepository{} +} +``` + +ポイント: + +* **ディレクトリとパッケージ名を一致させる**と分かりやすい。 +* `cmd/app/` の `main.go` はできるだけ薄くし、構造体やビジネスロジックは `internal/` や `pkg/` に置く。 + +### 6.3 テストファイルの分割 + +テストコードは基本的に同じパッケージか `package xxx_test` で書きます。 + +```go +// user/service_test.go +package user_test + +import ( + "testing" + + "example.com/project/internal/user" +) + +func TestService_Run(t *testing.T) { + repo := user.NewInMemoryRepository() + svc := user.NewService(repo) + + if err := svc.Run(); err != nil { + t.Fatalf("Run() error = %v", err) + } +} +``` + +## 7. gofmt / Lint / VSCode の設定 + +### 7.1 フォーマット・リンター + +1. **必須ツール** + + * `gofmt`(標準) + * `goimports`(インポートも自動整形) +2. **推奨リンター** + + * `golangci-lint`(多数のリンターを統合したツール) + +プロジェクトルートに `.golangci.yml` を置く例: + +```yaml +run: + timeout: 3m + tests: true + +linters: + enable: + - govet + - gofmt + - gosimple + - staticcheck + - unused + - errcheck + +issues: + exclude-use-default: false +``` + +### 7.2 VSCode の設定例 (`.vscode/settings.json`) + +```json +{ + "go.useLanguageServer": true, + "go.formatTool": "goimports", + "gopls": { + "staticcheck": true + }, + + "editor.formatOnSave": true, + "[go]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + + "go.lintTool": "golangci-lint", + "go.lintOnSave": "file", + "go.lintFlags": [ + "run", + "--fast" + ], + + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true +} +```