refactor(handler): optimize mutual follow checks and remove unused skills
- remove unused .kilocode Go patterns and testing skill files - simplify mutual follow status retrieval in post and user handlers - restrict mutual follow status fetching to self-viewing scenarios to reduce unnecessary service calls
This commit is contained in:
@@ -1,674 +0,0 @@
|
||||
---
|
||||
name: golang-patterns
|
||||
description: 用于构建健壮、高效且可维护的Go应用程序的惯用Go模式、最佳实践和约定。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Go 开发模式
|
||||
|
||||
用于构建健壮、高效和可维护应用程序的惯用 Go 模式与最佳实践。
|
||||
|
||||
## 何时激活
|
||||
|
||||
* 编写新的 Go 代码时
|
||||
* 审查 Go 代码时
|
||||
* 重构现有 Go 代码时
|
||||
* 设计 Go 包/模块时
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 简洁与清晰
|
||||
|
||||
Go 推崇简洁而非精巧。代码应该显而易见且易于阅读。
|
||||
|
||||
```go
|
||||
// Good: Clear and direct
|
||||
func GetUser(id string) (*User, error) {
|
||||
user, err := db.FindUser(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get user %s: %w", id, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Bad: Overly clever
|
||||
func GetUser(id string) (*User, error) {
|
||||
return func() (*User, error) {
|
||||
if u, e := db.FindUser(id); e == nil {
|
||||
return u, nil
|
||||
} else {
|
||||
return nil, e
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 让零值变得有用
|
||||
|
||||
设计类型时,应使其零值无需初始化即可立即使用。
|
||||
|
||||
```go
|
||||
// Good: Zero value is useful
|
||||
type Counter struct {
|
||||
mu sync.Mutex
|
||||
count int // zero value is 0, ready to use
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Good: bytes.Buffer works with zero value
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("hello")
|
||||
|
||||
// Bad: Requires initialization
|
||||
type BadCounter struct {
|
||||
counts map[string]int // nil map will panic
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 接受接口,返回结构体
|
||||
|
||||
函数应该接受接口参数并返回具体类型。
|
||||
|
||||
```go
|
||||
// Good: Accepts interface, returns concrete type
|
||||
func ProcessData(r io.Reader) (*Result, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Data: data}, nil
|
||||
}
|
||||
|
||||
// Bad: Returns interface (hides implementation details unnecessarily)
|
||||
func ProcessData(r io.Reader) (io.Reader, error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理模式
|
||||
|
||||
### 带上下文的错误包装
|
||||
|
||||
```go
|
||||
// Good: Wrap errors with context
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load config %s: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义错误类型
|
||||
|
||||
```go
|
||||
// Define domain-specific errors
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// Sentinel errors for common cases
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
```
|
||||
|
||||
### 使用 errors.Is 和 errors.As 检查错误
|
||||
|
||||
```go
|
||||
func HandleError(err error) {
|
||||
// Check for specific error
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
log.Println("No records found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for error type
|
||||
var validationErr *ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
log.Printf("Validation error on field %s: %s",
|
||||
validationErr.Field, validationErr.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
log.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 永不忽略错误
|
||||
|
||||
```go
|
||||
// Bad: Ignoring error with blank identifier
|
||||
result, _ := doSomething()
|
||||
|
||||
// Good: Handle or explicitly document why it's safe to ignore
|
||||
result, err := doSomething()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Acceptable: When error truly doesn't matter (rare)
|
||||
_ = writer.Close() // Best-effort cleanup, error logged elsewhere
|
||||
```
|
||||
|
||||
## 并发模式
|
||||
|
||||
### 工作池
|
||||
|
||||
```go
|
||||
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for job := range jobs {
|
||||
results <- process(job)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}
|
||||
```
|
||||
|
||||
### 用于取消和超时的 Context
|
||||
|
||||
```go
|
||||
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
```
|
||||
|
||||
### 优雅关闭
|
||||
|
||||
```go
|
||||
func GracefulShutdown(server *http.Server) {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
}
|
||||
```
|
||||
|
||||
### 用于协调 Goroutine 的 errgroup
|
||||
|
||||
```go
|
||||
import "golang.org/x/sync/errgroup"
|
||||
|
||||
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
results := make([][]byte, len(urls))
|
||||
|
||||
for i, url := range urls {
|
||||
i, url := i, url // Capture loop variables
|
||||
g.Go(func() error {
|
||||
data, err := FetchWithTimeout(ctx, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results[i] = data
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 避免 Goroutine 泄漏
|
||||
|
||||
```go
|
||||
// Bad: Goroutine leak if context is cancelled
|
||||
func leakyFetch(ctx context.Context, url string) <-chan []byte {
|
||||
ch := make(chan []byte)
|
||||
go func() {
|
||||
data, _ := fetch(url)
|
||||
ch <- data // Blocks forever if no receiver
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Good: Properly handles cancellation
|
||||
func safeFetch(ctx context.Context, url string) <-chan []byte {
|
||||
ch := make(chan []byte, 1) // Buffered channel
|
||||
go func() {
|
||||
data, err := fetch(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- data:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
```
|
||||
|
||||
## 接口设计
|
||||
|
||||
### 小而专注的接口
|
||||
|
||||
```go
|
||||
// Good: Single-method interfaces
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Compose interfaces as needed
|
||||
type ReadWriteCloser interface {
|
||||
Reader
|
||||
Writer
|
||||
Closer
|
||||
}
|
||||
```
|
||||
|
||||
### 在接口使用处定义接口
|
||||
|
||||
```go
|
||||
// In the consumer package, not the provider
|
||||
package service
|
||||
|
||||
// UserStore defines what this service needs
|
||||
type UserStore interface {
|
||||
GetUser(id string) (*User, error)
|
||||
SaveUser(user *User) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
store UserStore
|
||||
}
|
||||
|
||||
// Concrete implementation can be in another package
|
||||
// It doesn't need to know about this interface
|
||||
```
|
||||
|
||||
### 使用类型断言实现可选行为
|
||||
|
||||
```go
|
||||
type Flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
func WriteAndFlush(w io.Writer, data []byte) error {
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush if supported
|
||||
if f, ok := w.(Flusher); ok {
|
||||
return f.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 包组织
|
||||
|
||||
### 标准项目布局
|
||||
|
||||
```text
|
||||
myproject/
|
||||
├── cmd/
|
||||
│ └── myapp/
|
||||
│ └── main.go # Entry point
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP handlers
|
||||
│ ├── service/ # Business logic
|
||||
│ ├── repository/ # Data access
|
||||
│ └── config/ # Configuration
|
||||
├── pkg/
|
||||
│ └── client/ # Public API client
|
||||
├── api/
|
||||
│ └── v1/ # API definitions (proto, OpenAPI)
|
||||
├── testdata/ # Test fixtures
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
### 包命名
|
||||
|
||||
```go
|
||||
// Good: Short, lowercase, no underscores
|
||||
package http
|
||||
package json
|
||||
package user
|
||||
|
||||
// Bad: Verbose, mixed case, or redundant
|
||||
package httpHandler
|
||||
package json_parser
|
||||
package userService // Redundant 'Service' suffix
|
||||
```
|
||||
|
||||
### 避免包级状态
|
||||
|
||||
```go
|
||||
// Bad: Global mutable state
|
||||
var db *sql.DB
|
||||
|
||||
func init() {
|
||||
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
||||
}
|
||||
|
||||
// Good: Dependency injection
|
||||
type Server struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewServer(db *sql.DB) *Server {
|
||||
return &Server{db: db}
|
||||
}
|
||||
```
|
||||
|
||||
## 结构体设计
|
||||
|
||||
### 函数式选项模式
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
addr string
|
||||
timeout time.Duration
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Option func(*Server)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(s *Server) {
|
||||
s.timeout = d
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(l *log.Logger) Option {
|
||||
return func(s *Server) {
|
||||
s.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(addr string, opts ...Option) *Server {
|
||||
s := &Server{
|
||||
addr: addr,
|
||||
timeout: 30 * time.Second, // default
|
||||
logger: log.Default(), // default
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Usage
|
||||
server := NewServer(":8080",
|
||||
WithTimeout(60*time.Second),
|
||||
WithLogger(customLogger),
|
||||
)
|
||||
```
|
||||
|
||||
### 使用嵌入实现组合
|
||||
|
||||
```go
|
||||
type Logger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (l *Logger) Log(msg string) {
|
||||
fmt.Printf("[%s] %s\n", l.prefix, msg)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
*Logger // Embedding - Server gets Log method
|
||||
addr string
|
||||
}
|
||||
|
||||
func NewServer(addr string) *Server {
|
||||
return &Server{
|
||||
Logger: &Logger{prefix: "SERVER"},
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
s := NewServer(":8080")
|
||||
s.Log("Starting...") // Calls embedded Logger.Log
|
||||
```
|
||||
|
||||
## 内存与性能
|
||||
|
||||
### 当大小已知时预分配切片
|
||||
|
||||
```go
|
||||
// Bad: Grows slice multiple times
|
||||
func processItems(items []Item) []Result {
|
||||
var results []Result
|
||||
for _, item := range items {
|
||||
results = append(results, process(item))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Good: Single allocation
|
||||
func processItems(items []Item) []Result {
|
||||
results := make([]Result, 0, len(items))
|
||||
for _, item := range items {
|
||||
results = append(results, process(item))
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### 为频繁分配使用 sync.Pool
|
||||
|
||||
```go
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func ProcessRequest(data []byte) []byte {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
// Process...
|
||||
return buf.Bytes()
|
||||
}
|
||||
```
|
||||
|
||||
### 避免在循环中进行字符串拼接
|
||||
|
||||
```go
|
||||
// Bad: Creates many string allocations
|
||||
func join(parts []string) string {
|
||||
var result string
|
||||
for _, p := range parts {
|
||||
result += p + ","
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Good: Single allocation with strings.Builder
|
||||
func join(parts []string) string {
|
||||
var sb strings.Builder
|
||||
for i, p := range parts {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(p)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Best: Use standard library
|
||||
func join(parts []string) string {
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
```
|
||||
|
||||
## Go 工具集成
|
||||
|
||||
### 基本命令
|
||||
|
||||
```bash
|
||||
# Build and run
|
||||
go build ./...
|
||||
go run ./cmd/myapp
|
||||
|
||||
# Testing
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
go test -cover ./...
|
||||
|
||||
# Static analysis
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
golangci-lint run
|
||||
|
||||
# Module management
|
||||
go mod tidy
|
||||
go mod verify
|
||||
|
||||
# Formatting
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
```
|
||||
|
||||
### 推荐的 Linter 配置 (.golangci.yml)
|
||||
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- gofmt
|
||||
- goimports
|
||||
- misspell
|
||||
- unconvert
|
||||
- unparam
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
govet:
|
||||
check-shadowing: true
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
```
|
||||
|
||||
## 快速参考:Go 惯用法
|
||||
|
||||
| 惯用法 | 描述 |
|
||||
|-------|-------------|
|
||||
| 接受接口,返回结构体 | 函数接受接口参数,返回具体类型 |
|
||||
| 错误即值 | 将错误视为一等值,而非异常 |
|
||||
| 不要通过共享内存来通信 | 使用通道在 goroutine 之间进行协调 |
|
||||
| 让零值变得有用 | 类型应无需显式初始化即可工作 |
|
||||
| 少量复制优于少量依赖 | 避免不必要的外部依赖 |
|
||||
| 清晰优于精巧 | 优先考虑可读性而非精巧性 |
|
||||
| gofmt 虽非最爱,但却是每个人的朋友 | 始终使用 gofmt/goimports 格式化代码 |
|
||||
| 提前返回 | 先处理错误,保持主逻辑路径无缩进 |
|
||||
|
||||
## 应避免的反模式
|
||||
|
||||
```go
|
||||
// Bad: Naked returns in long functions
|
||||
func process() (result int, err error) {
|
||||
// ... 50 lines ...
|
||||
return // What is being returned?
|
||||
}
|
||||
|
||||
// Bad: Using panic for control flow
|
||||
func GetUser(id string) *User {
|
||||
user, err := db.Find(id)
|
||||
if err != nil {
|
||||
panic(err) // Don't do this
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Bad: Passing context in struct
|
||||
type Request struct {
|
||||
ctx context.Context // Context should be first param
|
||||
ID string
|
||||
}
|
||||
|
||||
// Good: Context as first parameter
|
||||
func ProcessRequest(ctx context.Context, id string) error {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Bad: Mixing value and pointer receivers
|
||||
type Counter struct{ n int }
|
||||
func (c Counter) Value() int { return c.n } // Value receiver
|
||||
func (c *Counter) Increment() { c.n++ } // Pointer receiver
|
||||
// Pick one style and be consistent
|
||||
```
|
||||
|
||||
**记住**:Go 代码应该以最好的方式显得“乏味”——可预测、一致且易于理解。如有疑问,保持简单。
|
||||
@@ -1,720 +0,0 @@
|
||||
---
|
||||
name: golang-testing
|
||||
description: Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Go Testing Patterns
|
||||
|
||||
Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing new Go functions or methods
|
||||
- Adding test coverage to existing code
|
||||
- Creating benchmarks for performance-critical code
|
||||
- Implementing fuzz tests for input validation
|
||||
- Following TDD workflow in Go projects
|
||||
|
||||
## TDD Workflow for Go
|
||||
|
||||
### The RED-GREEN-REFACTOR Cycle
|
||||
|
||||
```
|
||||
RED → Write a failing test first
|
||||
GREEN → Write minimal code to pass the test
|
||||
REFACTOR → Improve code while keeping tests green
|
||||
REPEAT → Continue with next requirement
|
||||
```
|
||||
|
||||
### Step-by-Step TDD in Go
|
||||
|
||||
```go
|
||||
// Step 1: Define the interface/signature
|
||||
// calculator.go
|
||||
package calculator
|
||||
|
||||
func Add(a, b int) int {
|
||||
panic("not implemented") // Placeholder
|
||||
}
|
||||
|
||||
// Step 2: Write failing test (RED)
|
||||
// calculator_test.go
|
||||
package calculator
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
got := Add(2, 3)
|
||||
want := 5
|
||||
if got != want {
|
||||
t.Errorf("Add(2, 3) = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Run test - verify FAIL
|
||||
// $ go test
|
||||
// --- FAIL: TestAdd (0.00s)
|
||||
// panic: not implemented
|
||||
|
||||
// Step 4: Implement minimal code (GREEN)
|
||||
func Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Step 5: Run test - verify PASS
|
||||
// $ go test
|
||||
// PASS
|
||||
|
||||
// Step 6: Refactor if needed, verify tests still pass
|
||||
```
|
||||
|
||||
## Table-Driven Tests
|
||||
|
||||
The standard pattern for Go tests. Enables comprehensive coverage with minimal code.
|
||||
|
||||
```go
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 2, 3, 5},
|
||||
{"negative numbers", -1, -2, -3},
|
||||
{"zero values", 0, 0, 0},
|
||||
{"mixed signs", -1, 1, 0},
|
||||
{"large numbers", 1000000, 2000000, 3000000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Add(tt.a, tt.b)
|
||||
if got != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Table-Driven Tests with Error Cases
|
||||
|
||||
```go
|
||||
func TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
input: `{"host": "localhost", "port": 8080}`,
|
||||
want: &Config{Host: "localhost", Port: 8080},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
input: `{invalid}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "minimal config",
|
||||
input: `{}`,
|
||||
want: &Config{}, // Zero value config
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseConfig(tt.input)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %+v; want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Subtests and Sub-benchmarks
|
||||
|
||||
### Organizing Related Tests
|
||||
|
||||
```go
|
||||
func TestUser(t *testing.T) {
|
||||
// Setup shared by all subtests
|
||||
db := setupTestDB(t)
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
user := &User{Name: "Alice"}
|
||||
err := db.CreateUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser failed: %v", err)
|
||||
}
|
||||
if user.ID == "" {
|
||||
t.Error("expected user ID to be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
user, err := db.GetUser("alice-id")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser failed: %v", err)
|
||||
}
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
// ...
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Subtests
|
||||
|
||||
```go
|
||||
func TestParallel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"case1", "input1"},
|
||||
{"case2", "input2"},
|
||||
{"case3", "input3"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // Capture range variable
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // Run subtests in parallel
|
||||
result := Process(tt.input)
|
||||
// assertions...
|
||||
_ = result
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Helpers
|
||||
|
||||
### Helper Functions
|
||||
|
||||
```go
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper() // Marks this as a helper function
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
// Cleanup when test finishes
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
// Run migrations
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
t.Fatalf("failed to create schema: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func assertNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t *testing.T, got, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Temporary Files and Directories
|
||||
|
||||
```go
|
||||
func TestFileProcessing(t *testing.T) {
|
||||
// Create temp directory - automatically cleaned up
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test file
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// Run test
|
||||
result, err := ProcessFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Assert...
|
||||
_ = result
|
||||
}
|
||||
```
|
||||
|
||||
## Golden Files
|
||||
|
||||
Testing against expected output files stored in `testdata/`.
|
||||
|
||||
```go
|
||||
var update = flag.Bool("update", false, "update golden files")
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input Template
|
||||
}{
|
||||
{"simple", Template{Name: "test"}},
|
||||
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Render(tt.input)
|
||||
|
||||
golden := filepath.Join("testdata", tt.name+".golden")
|
||||
|
||||
if *update {
|
||||
// Update golden file: go test -update
|
||||
err := os.WriteFile(golden, got, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to update golden file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
want, err := os.ReadFile(golden)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read golden file: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mocking with Interfaces
|
||||
|
||||
### Interface-Based Mocking
|
||||
|
||||
```go
|
||||
// Define interface for dependencies
|
||||
type UserRepository interface {
|
||||
GetUser(id string) (*User, error)
|
||||
SaveUser(user *User) error
|
||||
}
|
||||
|
||||
// Production implementation
|
||||
type PostgresUserRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
|
||||
// Real database query
|
||||
}
|
||||
|
||||
// Mock implementation for tests
|
||||
type MockUserRepository struct {
|
||||
GetUserFunc func(id string) (*User, error)
|
||||
SaveUserFunc func(user *User) error
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetUser(id string) (*User, error) {
|
||||
return m.GetUserFunc(id)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) SaveUser(user *User) error {
|
||||
return m.SaveUserFunc(user)
|
||||
}
|
||||
|
||||
// Test using mock
|
||||
func TestUserService(t *testing.T) {
|
||||
mock := &MockUserRepository{
|
||||
GetUserFunc: func(id string) (*User, error) {
|
||||
if id == "123" {
|
||||
return &User{ID: "123", Name: "Alice"}, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
service := NewUserService(mock)
|
||||
|
||||
user, err := service.GetUserProfile("123")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
### Basic Benchmarks
|
||||
|
||||
```go
|
||||
func BenchmarkProcess(b *testing.B) {
|
||||
data := generateTestData(1000)
|
||||
b.ResetTimer() // Don't count setup time
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Process(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Run: go test -bench=BenchmarkProcess -benchmem
|
||||
// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
|
||||
```
|
||||
|
||||
### Benchmark with Different Sizes
|
||||
|
||||
```go
|
||||
func BenchmarkSort(b *testing.B) {
|
||||
sizes := []int{100, 1000, 10000, 100000}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
data := generateRandomSlice(size)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Make a copy to avoid sorting already sorted data
|
||||
tmp := make([]int, len(data))
|
||||
copy(tmp, data)
|
||||
sort.Ints(tmp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Allocation Benchmarks
|
||||
|
||||
```go
|
||||
func BenchmarkStringConcat(b *testing.B) {
|
||||
parts := []string{"hello", "world", "foo", "bar", "baz"}
|
||||
|
||||
b.Run("plus", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var s string
|
||||
for _, p := range parts {
|
||||
s += p
|
||||
}
|
||||
_ = s
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("builder", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var sb strings.Builder
|
||||
for _, p := range parts {
|
||||
sb.WriteString(p)
|
||||
}
|
||||
_ = sb.String()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("join", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.Join(parts, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Fuzzing (Go 1.18+)
|
||||
|
||||
### Basic Fuzz Test
|
||||
|
||||
```go
|
||||
func FuzzParseJSON(f *testing.F) {
|
||||
// Add seed corpus
|
||||
f.Add(`{"name": "test"}`)
|
||||
f.Add(`{"count": 123}`)
|
||||
f.Add(`[]`)
|
||||
f.Add(`""`)
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal([]byte(input), &result)
|
||||
|
||||
if err != nil {
|
||||
// Invalid JSON is expected for random input
|
||||
return
|
||||
}
|
||||
|
||||
// If parsing succeeded, re-encoding should work
|
||||
_, err = json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s
|
||||
```
|
||||
|
||||
### Fuzz Test with Multiple Inputs
|
||||
|
||||
```go
|
||||
func FuzzCompare(f *testing.F) {
|
||||
f.Add("hello", "world")
|
||||
f.Add("", "")
|
||||
f.Add("abc", "abc")
|
||||
|
||||
f.Fuzz(func(t *testing.T, a, b string) {
|
||||
result := Compare(a, b)
|
||||
|
||||
// Property: Compare(a, a) should always equal 0
|
||||
if a == b && result != 0 {
|
||||
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
|
||||
}
|
||||
|
||||
// Property: Compare(a, b) and Compare(b, a) should have opposite signs
|
||||
reverse := Compare(b, a)
|
||||
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
|
||||
if result != 0 || reverse != 0 {
|
||||
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
|
||||
a, b, result, b, a, reverse)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Running Coverage
|
||||
|
||||
```bash
|
||||
# Basic coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Generate coverage profile
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
# View coverage in browser
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# View coverage by function
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
# Coverage with race detection
|
||||
go test -race -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
### Coverage Targets
|
||||
|
||||
| Code Type | Target |
|
||||
|-----------|--------|
|
||||
| Critical business logic | 100% |
|
||||
| Public APIs | 90%+ |
|
||||
| General code | 80%+ |
|
||||
| Generated code | Exclude |
|
||||
|
||||
### Excluding Generated Code from Coverage
|
||||
|
||||
```go
|
||||
//go:generate mockgen -source=interface.go -destination=mock_interface.go
|
||||
|
||||
// In coverage profile, exclude with build tags:
|
||||
// go test -cover -tags=!generate ./...
|
||||
```
|
||||
|
||||
## HTTP Handler Testing
|
||||
|
||||
```go
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
// Create request
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Call handler
|
||||
HealthHandler(w, req)
|
||||
|
||||
// Check response
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "OK" {
|
||||
t.Errorf("got body %q; want %q", body, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "get user",
|
||||
method: http.MethodGet,
|
||||
path: "/users/123",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"id":"123","name":"Alice"}`,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
method: http.MethodGet,
|
||||
path: "/users/999",
|
||||
wantStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "create user",
|
||||
method: http.MethodPost,
|
||||
path: "/users",
|
||||
body: `{"name":"Bob"}`,
|
||||
wantStatus: http.StatusCreated,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAPIHandler()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var body io.Reader
|
||||
if tt.body != "" {
|
||||
body = strings.NewReader(tt.body)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tt.method, tt.path, body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tt.wantStatus {
|
||||
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
|
||||
}
|
||||
|
||||
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
|
||||
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
go test ./...
|
||||
|
||||
# Run tests with verbose output
|
||||
go test -v ./...
|
||||
|
||||
# Run specific test
|
||||
go test -run TestAdd ./...
|
||||
|
||||
# Run tests matching pattern
|
||||
go test -run "TestUser/Create" ./...
|
||||
|
||||
# Run tests with race detector
|
||||
go test -race ./...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -cover -coverprofile=coverage.out ./...
|
||||
|
||||
# Run short tests only
|
||||
go test -short ./...
|
||||
|
||||
# Run tests with timeout
|
||||
go test -timeout 30s ./...
|
||||
|
||||
# Run benchmarks
|
||||
go test -bench=. -benchmem ./...
|
||||
|
||||
# Run fuzzing
|
||||
go test -fuzz=FuzzParse -fuzztime=30s ./...
|
||||
|
||||
# Count test runs (for flaky test detection)
|
||||
go test -count=10 ./...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
**DO:**
|
||||
- Write tests FIRST (TDD)
|
||||
- Use table-driven tests for comprehensive coverage
|
||||
- Test behavior, not implementation
|
||||
- Use `t.Helper()` in helper functions
|
||||
- Use `t.Parallel()` for independent tests
|
||||
- Clean up resources with `t.Cleanup()`
|
||||
- Use meaningful test names that describe the scenario
|
||||
|
||||
**DON'T:**
|
||||
- Test private functions directly (test through public API)
|
||||
- Use `time.Sleep()` in tests (use channels or conditions)
|
||||
- Ignore flaky tests (fix or remove them)
|
||||
- Mock everything (prefer integration tests when possible)
|
||||
- Skip error path testing
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Run tests
|
||||
run: go test -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Check coverage
|
||||
run: |
|
||||
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
|
||||
awk -F'%' '{if ($1 < 80) exit 1}'
|
||||
```
|
||||
|
||||
**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.
|
||||
@@ -1,291 +0,0 @@
|
||||
---
|
||||
name: use-modern-go
|
||||
description: Apply modern Go syntax guidelines based on project's Go version. Use when user ask for modern Go code guidelines.
|
||||
---
|
||||
|
||||
# Modern Go Guidelines
|
||||
|
||||
## Detected Go Version
|
||||
|
||||
!`grep -rh "^go " --include="go.mod" . 2>/dev/null | cut -d' ' -f2 | sort | uniq -c | sort -nr | head -1 | xargs | cut -d' ' -f2 | grep . || echo unknown`
|
||||
|
||||
## How to Use This Skill
|
||||
|
||||
DO NOT search for go.mod files or try to detect the version yourself. Use ONLY the version shown above.
|
||||
|
||||
**If version detected (not "unknown"):**
|
||||
- Say: "This project is using Go X.XX, so I’ll stick to modern Go best practices and freely use language features up to and including this version. If you’d prefer a different target version, just let me know."
|
||||
- Do NOT list features, do NOT ask for confirmation
|
||||
|
||||
**If version is "unknown":**
|
||||
- Say: "Could not detect Go version in this repository"
|
||||
- Use AskUserQuestion: "Which Go version should I target?" → [1.23] / [1.24] / [1.25] / [1.26]
|
||||
|
||||
**When writing Go code**, use ALL features from this document up to the target version:
|
||||
- Prefer modern built-ins and packages (`slices`, `maps`, `cmp`) over legacy patterns
|
||||
- Never use features from newer Go versions than the target
|
||||
- Never use outdated patterns when a modern alternative is available
|
||||
|
||||
---
|
||||
|
||||
## Features by Go Version
|
||||
|
||||
### Go 1.0+
|
||||
|
||||
- `time.Since`: `time.Since(start)` instead of `time.Now().Sub(start)`
|
||||
|
||||
### Go 1.8+
|
||||
|
||||
- `time.Until`: `time.Until(deadline)` instead of `deadline.Sub(time.Now())`
|
||||
|
||||
### Go 1.13+
|
||||
|
||||
- `errors.Is`: `errors.Is(err, target)` instead of `err == target` (works with wrapped errors)
|
||||
|
||||
### Go 1.18+
|
||||
|
||||
- `any`: Use `any` instead of `interface{}`
|
||||
- `bytes.Cut`: `before, after, found := bytes.Cut(b, sep)` instead of Index+slice
|
||||
- `strings.Cut`: `before, after, found := strings.Cut(s, sep)`
|
||||
|
||||
### Go 1.19+
|
||||
|
||||
- `fmt.Appendf`: `buf = fmt.Appendf(buf, "x=%d", x)` instead of `[]byte(fmt.Sprintf(...))`
|
||||
- `atomic.Bool`/`atomic.Int64`/`atomic.Pointer[T]`: Type-safe atomics instead of `atomic.StoreInt32`
|
||||
|
||||
```go
|
||||
var flag atomic.Bool
|
||||
flag.Store(true)
|
||||
if flag.Load() { ... }
|
||||
|
||||
var ptr atomic.Pointer[Config]
|
||||
ptr.Store(cfg)
|
||||
```
|
||||
|
||||
### Go 1.20+
|
||||
|
||||
- `strings.Clone`: `strings.Clone(s)` to copy string without sharing memory
|
||||
- `bytes.Clone`: `bytes.Clone(b)` to copy byte slice
|
||||
- `strings.CutPrefix/CutSuffix`: `if rest, ok := strings.CutPrefix(s, "pre:"); ok { ... }`
|
||||
- `errors.Join`: `errors.Join(err1, err2)` to combine multiple errors
|
||||
- `context.WithCancelCause`: `ctx, cancel := context.WithCancelCause(parent)` then `cancel(err)`
|
||||
- `context.Cause`: `context.Cause(ctx)` to get the error that caused cancellation
|
||||
|
||||
### Go 1.21+
|
||||
|
||||
**Built-ins:**
|
||||
- `min`/`max`: `max(a, b)` instead of if/else comparisons
|
||||
- `clear`: `clear(m)` to delete all map entries, `clear(s)` to zero slice elements
|
||||
|
||||
**slices package:**
|
||||
- `slices.Contains`: `slices.Contains(items, x)` instead of manual loops
|
||||
- `slices.Index`: `slices.Index(items, x)` returns index (-1 if not found)
|
||||
- `slices.IndexFunc`: `slices.IndexFunc(items, func(item T) bool { return item.ID == id })`
|
||||
- `slices.SortFunc`: `slices.SortFunc(items, func(a, b T) int { return cmp.Compare(a.X, b.X) })`
|
||||
- `slices.Sort`: `slices.Sort(items)` for ordered types
|
||||
- `slices.Max`/`slices.Min`: `slices.Max(items)` instead of manual loop
|
||||
- `slices.Reverse`: `slices.Reverse(items)` instead of manual swap loop
|
||||
- `slices.Compact`: `slices.Compact(items)` removes consecutive duplicates in-place
|
||||
- `slices.Clip`: `slices.Clip(s)` removes unused capacity
|
||||
- `slices.Clone`: `slices.Clone(s)` creates a copy
|
||||
|
||||
**maps package:**
|
||||
- `maps.Clone`: `maps.Clone(m)` instead of manual map iteration
|
||||
- `maps.Copy`: `maps.Copy(dst, src)` copies entries from src to dst
|
||||
- `maps.DeleteFunc`: `maps.DeleteFunc(m, func(k K, v V) bool { return condition })`
|
||||
|
||||
**sync package:**
|
||||
- `sync.OnceFunc`: `f := sync.OnceFunc(func() { ... })` instead of `sync.Once` + wrapper
|
||||
- `sync.OnceValue`: `getter := sync.OnceValue(func() T { return computeValue() })`
|
||||
|
||||
**context package:**
|
||||
- `context.AfterFunc`: `stop := context.AfterFunc(ctx, cleanup)` runs cleanup on cancellation
|
||||
- `context.WithTimeoutCause`: `ctx, cancel := context.WithTimeoutCause(parent, d, err)`
|
||||
- `context.WithDeadlineCause`: Similar with deadline instead of duration
|
||||
|
||||
### Go 1.22+
|
||||
|
||||
**Loops:**
|
||||
- `for i := range n`: `for i := range len(items)` instead of `for i := 0; i < len(items); i++`
|
||||
- Loop variables are now safe to capture in goroutines (each iteration has its own copy)
|
||||
|
||||
**cmp package:**
|
||||
- `cmp.Or`: `cmp.Or(flag, env, config, "default")` returns first non-zero value
|
||||
|
||||
```go
|
||||
// Instead of:
|
||||
name := os.Getenv("NAME")
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
// Use:
|
||||
name := cmp.Or(os.Getenv("NAME"), "default")
|
||||
```
|
||||
|
||||
**reflect package:**
|
||||
- `reflect.TypeFor`: `reflect.TypeFor[T]()` instead of `reflect.TypeOf((*T)(nil)).Elem()`
|
||||
|
||||
**net/http:**
|
||||
- Enhanced `http.ServeMux` patterns: `mux.HandleFunc("GET /api/{id}", handler)` with method and path params
|
||||
- `r.PathValue("id")` to get path parameters
|
||||
|
||||
### Go 1.23+
|
||||
|
||||
- `maps.Keys(m)` / `maps.Values(m)` return iterators
|
||||
- `slices.Collect(iter)` not manual loop to build slice from iterator
|
||||
- `slices.Sorted(iter)` to collect and sort in one step
|
||||
|
||||
```go
|
||||
keys := slices.Collect(maps.Keys(m)) // not: for k := range m { keys = append(keys, k) }
|
||||
sortedKeys := slices.Sorted(maps.Keys(m)) // collect + sort
|
||||
for k := range maps.Keys(m) { process(k) } // iterate directly
|
||||
```
|
||||
|
||||
**time package**
|
||||
|
||||
- `time.Tick`: Use `time.Tick` freely — as of Go 1.23, the garbage collector can recover unreferenced tickers, even if they haven't been stopped. The Stop method is no longer necessary to help the garbage collector. There is no longer any reason to prefer NewTicker when Tick will do.
|
||||
|
||||
### Go 1.24+
|
||||
|
||||
- `t.Context()` not `context.WithCancel(context.Background())` in tests.
|
||||
ALWAYS use t.Context() when a test function needs a context.
|
||||
|
||||
Before:
|
||||
```go
|
||||
func TestFoo(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
result := doSomething(ctx)
|
||||
}
|
||||
```
|
||||
After:
|
||||
```go
|
||||
func TestFoo(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
result := doSomething(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
- `omitzero` not `omitempty` in JSON struct tags.
|
||||
ALWAYS use omitzero for time.Duration, time.Time, structs, slices, maps.
|
||||
|
||||
Before:
|
||||
```go
|
||||
type Config struct {
|
||||
Timeout time.Duration `json:"timeout,omitempty"` // doesn't work for Duration!
|
||||
}
|
||||
```
|
||||
After:
|
||||
```go
|
||||
type Config struct {
|
||||
Timeout time.Duration `json:"timeout,omitzero"`
|
||||
}
|
||||
```
|
||||
|
||||
- `b.Loop()` not `for i := 0; i < b.N; i++` in benchmarks.
|
||||
ALWAYS use b.Loop() for the main loop in benchmark functions.
|
||||
|
||||
Before:
|
||||
```go
|
||||
func BenchmarkFoo(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
doWork()
|
||||
}
|
||||
}
|
||||
```
|
||||
After:
|
||||
```go
|
||||
func BenchmarkFoo(b *testing.B) {
|
||||
for b.Loop() {
|
||||
doWork()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `strings.SplitSeq` not `strings.Split` when iterating.
|
||||
ALWAYS use SplitSeq/FieldsSeq when iterating over split results in a for-range loop.
|
||||
|
||||
Before:
|
||||
```go
|
||||
for _, part := range strings.Split(s, ",") {
|
||||
process(part)
|
||||
}
|
||||
```
|
||||
After:
|
||||
```go
|
||||
for part := range strings.SplitSeq(s, ",") {
|
||||
process(part)
|
||||
}
|
||||
```
|
||||
Also: `strings.FieldsSeq`, `bytes.SplitSeq`, `bytes.FieldsSeq`.
|
||||
|
||||
### Go 1.25+
|
||||
|
||||
- `wg.Go(fn)` not `wg.Add(1)` + `go func() { defer wg.Done(); ... }()`.
|
||||
ALWAYS use wg.Go() when spawning goroutines with sync.WaitGroup.
|
||||
|
||||
Before:
|
||||
```go
|
||||
var wg sync.WaitGroup
|
||||
for _, item := range items {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
process(item)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
After:
|
||||
```go
|
||||
var wg sync.WaitGroup
|
||||
for _, item := range items {
|
||||
wg.Go(func() {
|
||||
process(item)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
### Go 1.26+
|
||||
|
||||
- `new(val)` not `x := val; &x` — returns pointer to any value.
|
||||
Go 1.26 extends new() to accept expressions, not just types.
|
||||
Type is inferred: new(0) → *int, new("s") → *string, new(T{}) → *T.
|
||||
DO NOT use `x := val; &x` pattern — always use new(val) directly.
|
||||
DO NOT use redundant casts like new(int(0)) — just write new(0).
|
||||
Common use case: struct fields with pointer types.
|
||||
|
||||
Before:
|
||||
```go
|
||||
timeout := 30
|
||||
debug := true
|
||||
cfg := Config{
|
||||
Timeout: &timeout,
|
||||
Debug: &debug,
|
||||
}
|
||||
```
|
||||
After:
|
||||
```go
|
||||
cfg := Config{
|
||||
Timeout: new(30), // *int
|
||||
Debug: new(true), // *bool
|
||||
}
|
||||
```
|
||||
|
||||
- `errors.AsType[T](err)` not `errors.As(err, &target)`.
|
||||
ALWAYS use errors.AsType when checking if error matches a specific type.
|
||||
|
||||
Before:
|
||||
```go
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
handle(pathErr)
|
||||
}
|
||||
```
|
||||
After:
|
||||
```go
|
||||
if pathErr, ok := errors.AsType[*os.PathError](err); ok {
|
||||
handle(pathErr)
|
||||
}
|
||||
```
|
||||
@@ -102,14 +102,16 @@ func (h *PostHandler) GetByID(c *gin.Context) {
|
||||
// 获取交互状态
|
||||
isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, currentUserID)
|
||||
|
||||
// 如果有当前用户,检查与帖子作者的相互关注状态
|
||||
var authorWithFollowStatus *dto.UserResponse
|
||||
if currentUserID != "" && post.User != nil {
|
||||
_, isFollowing, isFollowingMe, err := h.userService.GetUserByIDWithMutualFollowStatus(c.Request.Context(), post.UserID, currentUserID)
|
||||
if err == nil {
|
||||
authorWithFollowStatus = dto.ConvertUserToResponseWithMutualFollow(post.User, isFollowing, isFollowingMe)
|
||||
if post.User != nil {
|
||||
if currentUserID != "" {
|
||||
_, isFollowing, isFollowingMe, err := h.userService.GetUserByIDWithMutualFollowStatus(c.Request.Context(), post.UserID, currentUserID)
|
||||
if err == nil {
|
||||
authorWithFollowStatus = dto.ConvertUserToResponseWithMutualFollow(post.User, isFollowing, isFollowingMe)
|
||||
} else {
|
||||
authorWithFollowStatus = dto.ConvertUserToResponse(post.User)
|
||||
}
|
||||
} else {
|
||||
// 如果出错,使用默认的author
|
||||
authorWithFollowStatus = dto.ConvertUserToResponse(post.User)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,23 +635,19 @@ func (h *UserHandler) GetFollowingList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已登录,获取双向关注状态和实时计算的帖子数量
|
||||
var userResponses []*dto.UserResponse
|
||||
if currentUserID != "" && len(users) > 0 {
|
||||
userIDs := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userIDs[i] = u.ID
|
||||
}
|
||||
statusMap, _ := h.userService.GetMutualFollowStatus(c.Request.Context(), currentUserID, userIDs)
|
||||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, statusMap, postsCountMap)
|
||||
} else if len(users) > 0 {
|
||||
if len(users) > 0 {
|
||||
userIDs := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userIDs[i] = u.ID
|
||||
}
|
||||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||||
if currentUserID != "" && userID == currentUserID {
|
||||
statusMap, _ := h.userService.GetMutualFollowStatus(c.Request.Context(), currentUserID, userIDs)
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, statusMap, postsCountMap)
|
||||
} else {
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||||
}
|
||||
} else {
|
||||
userResponses = dto.ConvertUsersToResponse(users)
|
||||
}
|
||||
@@ -685,23 +681,19 @@ func (h *UserHandler) GetFollowersList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已登录,获取双向关注状态和实时计算的帖子数量
|
||||
var userResponses []*dto.UserResponse
|
||||
if currentUserID != "" && len(users) > 0 {
|
||||
userIDs := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userIDs[i] = u.ID
|
||||
}
|
||||
statusMap, _ := h.userService.GetMutualFollowStatus(c.Request.Context(), currentUserID, userIDs)
|
||||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, statusMap, postsCountMap)
|
||||
} else if len(users) > 0 {
|
||||
if len(users) > 0 {
|
||||
userIDs := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userIDs[i] = u.ID
|
||||
}
|
||||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||||
if currentUserID != "" && userID == currentUserID {
|
||||
statusMap, _ := h.userService.GetMutualFollowStatus(c.Request.Context(), currentUserID, userIDs)
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, statusMap, postsCountMap)
|
||||
} else {
|
||||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||||
}
|
||||
} else {
|
||||
userResponses = dto.ConvertUsersToResponse(users)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user