Go语言高效学习-项目实战(Days 8-14)

针对NodeJS工程师的Go语言学习计划
🏗️ 阶段三:项目实战(Days 8-14)
目标:实战中熟悉大型项目布局与协作者工具
Days 8-10:标准项目结构

🚀 Go语言高效学习计划(NodeJS工程师版)

目标:2周快速掌握核心概念,上手大型项目;后续深入高级特性

本文涉及的代码链接:Github

知识点梳理与对比

标准项目结构

Go 社区对项目结构有一些约定俗成的规范,虽然不像 Java 那样强制,但遵循这些规范可以提高代码的可读性、可维护性和可测试性。

1. 企业级布局规范

一个典型的 Go 企业级项目通常包含以下几个顶级目录:

  • cmd/: 存放项目的可执行文件(入口点)。每个子目录对应一个独立的可执行程序。
    • 例如:cmd/api/main.go (HTTP API 服务), cmd/worker/main.go (后台任务)
    • 与 Node.js 对比: 类似于 Node.js 项目中的 bin/scripts/ 目录。
  • pkg/: 存放可被外部项目导入和使用的公共库代码。这些代码是你的项目的公共 API。
    • 例如:pkg/models/user.go (用户模型), pkg/utils/http.go (HTTP 工具函数)
    • 与 Node.js 对比: 类似于发布到 npm 的模块,其他项目可以通过 import "yourproject/pkg/..." 引用。
  • internal/: 存放仅限项目内部使用的私有代码。这些代码不会被外部项目导入。
    • 例如:internal/auth/jwt.go (JWT 认证), internal/db/mysql.go (MySQL 连接)
    • 与 Node.js 对比: Node.js 没有强制的私有目录概念,但通常通过命名约定(如 _ 前缀)或模块作用域来实现类似效果。Go 的 internal/ 机制是编译器强制的,更安全。
  • api/: 如果构建的是一个web service,用于存放对外提供的api定义,比如说像是proto文件.
    • 例如:api/v1/user.proto
    • 与 Node.js 对比: 类似于项目中的 api//v1/ 目录。
  • 其他常见目录:
    • configs/: 配置文件(通常有多个环境)
    • docs/: 项目文档
    • scripts/: 各种脚本(构建、部署等)
    • test/: 额外的集成测试或性能测试
    • vendor/: 项目依赖(通过 Go Modules 管理,通常不需要手动修改)

2. 目录职责示例

myproject/
├── cmd/
│   └── api/
│       └── main.go  // HTTP API 服务入口
├── pkg/
│   ├── models/
│   │   └── user.go   // 用户模型定义
│   └── utils/
│       └── http.go  // HTTP 工具函数
├── internal/
│   ├── auth/
│   │   └── jwt.go   // JWT 认证逻辑
│   └── db/
│       └── mysql.go // MySQL 数据库连接
├── api
│    └── v1
│       └── user.proto //用户 v1 api 定义
├── configs/
│   └── config.yaml      // 配置文件
└── go.mod           // Go Modules 文件

3. 依赖注入框架 (Wire)

依赖注入 (DI) 是一种解耦组件之间依赖关系的设计模式。在 Go 中,常用的 DI 框架包括:

  • Wire (Google 出品): 通过代码生成实现编译时依赖注入,性能好,类型安全。
  • Dig (Uber 出品): 基于反射实现运行时依赖注入,使用方便,但性能稍差。

为什么使用 Wire?

  • 编译时检查: Wire 在编译时生成依赖注入代码,可以及早发现依赖问题,避免运行时错误。
  • 性能优势: 没有反射开销,性能更好。
  • 类型安全: 依赖关系在代码中明确指定,类型错误会在编译时暴露。

Wire 基本概念

  • Provider: 提供依赖对象的函数。
  • Injector: 根据 Provider 生成依赖注入代码的函数。
  • wire.Build(): Wire 的核心函数,用于指定 Provider 集合。

Wire 示例

假设我们有以下三个组件:

// internal/db/mysql.go
package db

import "database/sql"

type Config struct {
	DSN string
}

func NewDB(cfg Config) (*sql.DB, error) {
	// ... 连接数据库 ...
}

// pkg/models/user.go
package models

import "database/sql"

type User struct {
	ID   int
	Name string
}

type UserRepository struct {
	db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
	return &UserRepository{db: db}
}

// cmd/api/main.go
package main

import (
	"myproject/internal/db"
	"myproject/pkg/models"
	"net/http"
)

type Server struct {
	userRepo *models.UserRepository
}

func NewServer(userRepo *models.UserRepository) *Server {
	return &Server{userRepo: userRepo}
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// ... 处理 HTTP 请求 ...
}

func main() {
	// ... 手动创建依赖 ...
	// dbConfig := db.Config{DSN: "..."}
	// dbConn, err := db.NewDB(dbConfig)
	// userRepo := models.NewUserRepository(dbConn)
	// server := NewServer(userRepo)
	// http.ListenAndServe(":8080", server)
}

使用 Wire 改造:

  1. 创建 wire.go 文件 (通常放在 cmd/api/ 目录下):
//go:build wireinject
// +build wireinject

package main

import (
	"database/sql"
	"myproject/internal/db"
	"myproject/pkg/models"

	"github.com/google/wire"
)

func InitializeServer() (*Server, error) {
	wire.Build(
		db.NewDB,
		models.NewUserRepository,
		NewServer,
		wire.Bind(new(interface{}), new(*sql.DB)), // 接口绑定(如果需要)
		db.Config{DSN: "..."} // 直接提供配置值
	)
	return &Server{}, nil // wire 会自动填充返回值
}

添加空实现,让wire可以生成代码

//+build !wireinject

package main
func InitializeServer() (*Server, error) {
	panic("implement me")
}
  1. 运行 wire 命令生成代码:

    go install github.com/google/wire/cmd/wire@latest
    cd cmd/api
    wire
    

    这会在 cmd/api/ 目录下生成 wire_gen.go 文件,其中包含了依赖注入代码。

  2. 修改 main.go 使用生成的 InitializeServer 函数:

package main

import (
	"net/http"
)

func main() {
	server, err := InitializeServer() // 使用 Wire 生成的函数
	if err != nil {
		panic(err)
	}
	http.ListenAndServe(":8080", server)
}

实战:重构代码

现在,我们将以上述示例为基础,构建一个完整的可运行的 HTTP API 服务,实现用户查询功能。

1. 项目结构

userapi/
├── cmd/
│   └── api/
│       ├── main.go
│       └── wire.go
│       └── wire_gen.go
├── pkg/
│   ├── models/
│   │   └── user.go
│   └── handlers/
│       └── user.go  // 将 HTTP 处理逻辑分离
├── internal/
│   └── db/
        └── user.go
│       └── mysql.go
├── configs/
│   └── config.yaml
├── go.mod
└── go.sum

2. 代码实现

  • configs/config.yaml:
db:
  dsn: "user:password@tcp(localhost:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local"

  • internal/db/mysql.go:
package db

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql" // 导入 MySQL 驱动
	"github.com/spf13/viper"
)

type Config struct {
	DSN string
}

func NewDBConfig() (Config, error) {

	cfg := Config{
		DSN: viper.GetString("db.dsn"), // 从 Viper 读取配置
	}
	fmt.Println("db_cfg,", cfg)
	return cfg, nil
}

func NewDB(cfg Config) (*sql.DB, error) {
	db, err := sql.Open("mysql", cfg.DSN)
	if err != nil {
		return nil, err
	}
	if err := db.Ping(); err != nil {
		return nil, err
	}
	return db, nil
}

  • internal/db/user.go:
package db

import (
	"database/sql"
	"myproject/pkg/models"
)

// UserRepository 实现 pkg/models 中定义的 UserRepository 接口
type UserRepository struct {
	db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
	return &UserRepository{db: db}
}

func (r *UserRepository) GetUserByID(id int) (*models.User, error) {
	user := &models.User{}
	err := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name)
	if err != nil {
		return nil, err
	}
	return user, nil
}
  • pkg/models/user.go:
package models
//定义 models 的接口
type UserRepository interface {

	GetUserByID(id int) (*User,error)
}
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}
  • pkg/handlers/user.go:
package handlers

import (
	"encoding/json"
	"myproject/pkg/models"
	"net/http"
	"strconv"

	"github.com/gorilla/mux" // 使用 gorilla/mux 路由库
)

type UserHandler struct {
	userRepo models.UserRepository
}

func NewUserHandler(userRepo models.UserRepository) *UserHandler {
	return &UserHandler{userRepo: userRepo}
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	idStr := vars["id"]
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	user, err := h.userRepo.GetUserByID(id)
	if err != nil {
		http.Error(w, "User not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

  • cmd/api/wire.go:
//+build wireinject

package main

import (
	"database/sql"
	"myproject/internal/db"
	"myproject/pkg/handlers"
	"myproject/pkg/models"

	"github.com/google/wire"
)

func InitializeUserHandler() (*handlers.UserHandler, error) {
	wire.Build(
		db.NewDB,
		db.NewDBConfig,
		db.NewUserRepository,
		handlers.NewUserHandler,
		wire.Bind(new(models.UserRepository), new(*db.UserRepository)),
	)
	return &handlers.UserHandler{}, nil
}

添加空实现,让wire可以生成代码

//+build !wireinject

package main

import "myproject/pkg/handlers"

func InitializeUserHandler() (*handlers.UserHandler, error) {
	panic("implement me")
}
  • cmd/api/wire_gen.go:
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
	"myproject/internal/db"
	"myproject/pkg/handlers"
)

// Injectors from wire.go:

func InitializeUserHandler() (*handlers.UserHandler, error) {
	config, err := db.NewDBConfig()
	if err != nil {
		return nil, err
	}
	sqlDB, err := db.NewDB(config)
	if err != nil {
		return nil, err
	}
	userRepository := db.NewUserRepository(sqlDB)
	userHandler := handlers.NewUserHandler(userRepository)
	return userHandler, nil
}
  • cmd/api/main.go:
package main

import (
	"log"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/spf13/viper"
)

func main() {

	// 初始化 Viper
	viper.SetConfigName("config") // 配置文件名(不带扩展名)
	viper.SetConfigType("yaml")   // 配置文件类型
	viper.AddConfigPath("./configs")  // 配置文件路径
	err := viper.ReadInConfig()   // 读取配置文件
	if err != nil {               // 处理读取错误
		panic(err)
	}

	userHandler, err := InitializeUserHandler() // 使用 Wire 生成的函数
	if err != nil {
		panic(err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/users/{id}", userHandler.GetUser).Methods("GET")

	log.Println("Server listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", r))
}

  • go.mod:
module myproject

go 1.20

require (
	github.com/go-sql-driver/mysql v1.7.1
	github.com/google/wire v0.5.0
	github.com/gorilla/mux v1.8.1
	github.com/spf13/viper v1.18.2
)

require (
	github.com/fsnotify/fsnotify v1.7.0 // indirect
	github.com/hashicorp/hcl v1.0.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/magiconair/properties v1.8.7 // indirect
	github.com/mitchellh/mapstructure v1.5.0 // indirect
	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
	github.com/sagikazarmark/locafero v0.4.0 // indirect
	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
	github.com/sourcegraph/conc v0.3.0 // indirect
	github.com/spf13/afero v1.11.0 // indirect
	github.com/spf13/cast v1.6.0 // indirect
	github.com/spf13/cobra v1.8.0 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	github.com/subosito/gotenv v1.6.0 // indirect
	go.uber.org/atomic v1.9.0 // indirect
	go.uber.org/multierr v1.9.0 // indirect
	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
	golang.org/x/sys v0.15.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	gopkg.in/ini.v1 v1.67.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

3. 运行步骤

  1. 初始化项目:

    go mod init myproject
    go mod tidy
    
    • 确保你本地已经有了mysql服务。
  2. 生成 Wire 代码:

     go install github.com/google/wire/cmd/wire@latest
    cd cmd/api
    wire
    
  3. 运行项目:

    go run ./cmd/api
    
  4. 在数据库里增加users
CREATE TABLE `users` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `users` (`id`, `name`) VALUES
(1, 'Alice'),
(2, 'Bob');
  1. 测试 API:

    curl http://localhost:8080/users/1
    # 输出: {"id":1,"name":"Alice"}
    
    curl http://localhost:8080/users/2
    # 输出: {"id":2,"name":"Bob"}
    

希望以上详细解释和实战项目能帮助您更好地理解 Go 的项目结构和依赖注入!