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 同步。
- 主 goroutine 收集文件列表,创建带缓冲的 jobs channel(存放文件路径),results channel(存放每个文件的统计结果)。
- 启动固定数量的 worker goroutine,从 jobs 中取文件,读取并统计关键词,将结果发送到 results。
- 发送完所有任务后关闭 jobs channel,worker 退出后关闭 results channel。
- 使用
context.WithTimeout控制总处理时间,超时后取消所有 worker。 - 统计结果汇总打印。
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匹配或正则,视情况而定。
浙公网安备 33010602011771号