15、Go泛型

15、Go 泛型


Go 1.18+ 泛型基础

Go 1.18 引入了类型参数(Type Parameters),允许编写可以处理多种类型的函数和类型,同时保持编译时类型安全。

泛型函数

定义泛型函数时,在函数名后面使用方括号列出类型参数,然后在参数和返回值中使用。

语法:

func FuncName[T constraint](param T) T { ... }

最简单的例子 —— 返回任意类型切片的第一个元素:

func First[T any](s []T) T {
    if len(s) == 0 {
        var zero T
        return zero
    }
    return s[0]
}

any 是 interface{} 的别名,表示没有任何约束,可以匹配任何类型。var zero T 会得到该类型的零值。

泛型类型

除了函数,也可以定义泛型结构体、接口等。

type Stack[T any] struct {
    elements []T
}

func (s *Stack[T]) Push(v T) {
    s.elements = append(s.elements, v)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
    v := s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1]
    return v, true
}

使用时指定具体类型:var intStack Stack[int]。

推断类型参数

很多时候 Go 编译器能自动推断类型参数,可以省略显式指定:

ints := []int{1, 2, 3}
first := First(ints)  // 自动推断 T = int

通用的 Map、Filter、Reduce

这三个函数是函数式编程中常用的集合操作,使用泛型可以写出一个实现用于任意类型的切片。

Map:将一种切片映射为另一种切片

接收一个切片和转换函数,返回新切片。

func Map[T any, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

使用:

nums := []int{1, 2, 3}
strs := Map(nums, func(n int) string {
    return fmt.Sprintf("num:%d", n)
})
// ["num:1", "num:2", "num:3"]

Filter:过滤切片中满足条件的元素

func Filter[T any](s []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range s {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

使用:

nums := []int{1, 2, 3, 4, 5}
even := Filter(nums, func(n int) bool { return n%2 == 0 })
// [2, 4]

Reduce:将切片归约为一个值

func Reduce[T any, U any](s []T, init U, reducer func(U, T) U) U {
    acc := init
    for _, v := range s {
        acc = reducer(acc, v)
    }
    return acc
}

使用:

nums := []int{1, 2, 3, 4, 5}
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n }) // 15
product := Reduce(nums, 1, func(acc, n int) int { return acc * n }) // 120

用泛型简化代码的收益:不需要为 int、string、MyStruct 等重复写同样的逻辑,一套代码全部搞定,且编译器能检查类型。

泛型约束

约束(Constraint)是对类型参数的限制,告诉编译器这个类型必须支持哪些操作。

any 和 comparable

  • any:等价于 interface{},无任何限制,不能做比较(==)或排序等。
  • comparable:Go 内置的预声明接口,允许 == 和 != 操作。可比较的类型(包括基本类型、指针、结构体字段全部可比较等)都满足。
// 只能用于可比较的类型,比如判断切片是否包含某个元素
func Contains[T comparable](s []T, v T) bool {
    for _, x := range s {
        if x == v {
            return true
        }
    }
    return false
}

自定义约束接口

可以通过定义接口来创建自定义约束。这种接口可以包含方法和类型集合。

例:要求类型有 String() string 方法

type Stringer interface {
    String() string
}

func Concat[T Stringer](elems []T) string {
    var result string
    for _, e := range elems {
        result += e.String()
    }
    return result
}

例:联合约束 —— 只允许特定基础类型

// 只允许 int、float64、string
type Number interface {
    int | float64 | string
}

func Add[T Number](a, b T) T {
    return a + b
}

注意:string 的 + 是拼接,所以这个约束只适用于支持 + 操作的类型。更常见的数值约束是:

type RealNumber interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

func Max[T RealNumber](a, b T) T {
    if a > b {
        return a
    }
    return b
}

也可以使用 golang.org/x/exp/constraints 包中预定义的 constraints.Ordered(支持比较)和 constraints.Integer、constraints.Float 等,但标准库已在 Go 1.21 开始将部分约束移至标准库 cmp 包。

comparable 的陷阱

comparable 不能用于任何包含不可比较字段(如切片、map、函数)的类型。编译时会自动检查。

自定义可比较约束:

type MyStruct struct {
    Name string
    ID   int
}

// MyStruct 的所有字段都是可比较的,因此 MyStruct 满足 comparable
Contains([]MyStruct{{"A",1},{"B",2}}, MyStruct{"A",1}) // true

阶段项目:并发日志分析器

需求:给定多个日志文件路径,并发读取每个文件,统计多个关键词的出现频率,支持超时控制,输出每个文件的结果。

技术栈:Worker 池、context 超时、bufio 逐行读取、sync 同步。

  1. 主 goroutine 收集文件列表,创建带缓冲的 jobs channel(存放文件路径),results channel(存放每个文件的统计结果)。
  2. 启动固定数量的 worker goroutine,从 jobs 中取文件,读取并统计关键词,将结果发送到 results。
  3. 发送完所有任务后关闭 jobs channel,worker 退出后关闭 results channel。
  4. 使用 context.WithTimeout 控制总处理时间,超时后取消所有 worker。
  5. 统计结果汇总打印。
type LogResult struct {
    File    string
    Counts  map[string]int // 关键词 -> 出现次数
    Err     error
}

func worker(ctx context.Context, keywords []string, jobs <-chan string, results chan<- LogResult) {
    for path := range jobs {
        // 检查 context 是否已取消
        select {
        case <-ctx.Done():
            return
        default:
        }

        counts, err := processFile(path, keywords)
        results <- LogResult{
            File:   path,
            Counts: counts,
            Err:    err,
        }
    }
}

func processFile(path string, keywords []string) (map[string]int, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    counts := make(map[string]int)
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        line := scanner.Text()
        for _, kw := range keywords {
            counts[kw] += strings.Count(line, kw)
        }
    }
    return counts, scanner.Err()
}

func AnalyzeLogs(filePaths []string, keywords []string, timeout time.Duration) ([]LogResult, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    numWorkers := runtime.NumCPU() // 可根据需要调整
    jobs := make(chan string, len(filePaths))
    results := make(chan LogResult, len(filePaths))

    // 启动 worker 池
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            worker(ctx, keywords, jobs, results)
        }()
    }

    // 分发任务
    for _, p := range filePaths {
        jobs <- p
    }
    close(jobs)

    // 等待所有 worker 完成,然后关闭 results
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集结果
    var output []LogResult
    for res := range results {
        output = append(output, res)
    }

    // 检查是否因为超时导致一些任务被跳过
    if ctx.Err() != nil {
        return output, ctx.Err()
    }
    return output, nil
}

func main() {
    files := []string{"app1.log", "app2.log", "app3.log"}
    keywords := []string{"ERROR", "WARN", "panic"}

    results, err := AnalyzeLogs(files, keywords, 5*time.Second)
    if err != nil {
        fmt.Println("Analysis completed with error:", err)
    }
    for _, r := range results {
        if r.Err != nil {
            fmt.Printf("File %s: error %v\n", r.File, r.Err)
        } else {
            fmt.Printf("File %s: %v\n", r.File, r.Counts)
        }
    }
}
  • 使用带缓冲的 jobs 和 results 通道避免 goroutine 阻塞。
  • Worker 内通过 select 检查 ctx.Done(),一旦超时立即停止读取新文件。
  • 使用 bufio.Scanner 逐行处理,内存友好,适合大日志文件。
  • 关键词计数用 strings.Count,简单直接;也可改用 string 匹配或正则,视情况而定。
posted @ 2026-09-24 16:46  爱喝奶茶的CC怪  阅读(10)  评论(0)    收藏  举报