Func

函数是 Go 代码复用的基本单元。本节介绍三种常用的函数使用模式:不定长参数、指针传参和函数作为值传递,这些都是 Web3 开发中封装工具函数的基础。

不定长参数与空接口

golang 函数中允许传递不定长的数据,或者传递 interface{} 空接口,用来接收任意类型的数据。

  • args ...interface{} 表示任意数量、任意类型的参数,args 在函数体内是一个切片
  • 遍历时可以用类型断言 v.(type) 区分具体类型(需在 switch 语句中使用)
package main

import (
	"fmt"
	"math/big"
)

func main() {
	type Tree struct {
		leaves int
		name   string
	}
	myFunc(1, big.NewInt(23), Tree{99, "trace"}, "hello world", []byte("hello"))
}
func myFunc(args ...interface{}) {
	for _, v := range args {
		fmt.Println(v)
	}
}

输出:

1
23
{99 trace}
hello world
[104 101 108 108 111]

指针传参

函数之间传递指针内存地址可以共同操作相同的数据,避免 copy data 带来的拷贝负担。

Go 中所有传参都是值传递,指针传递的其实是「指针的值」——副本指向同一片内存,因此函数内对 *a 的修改对外部可见。大结构体传参时传指针可以显著减少拷贝开销:

package main

import (
	"fmt"
)

func main() {
	a := 1
	b := add(&a)
	fmt.Println(a) // 2,a 被 add 函数修改
	fmt.Println(b) // 2
}

func add(a *int) (addone int) {
	*a = *a + 1
	addone = *a
	return
}

函数作为值传递

函数也可以作为参数传参,处理特定的业务逻辑。这在 Go 中称为「一等函数」——函数可以像普通值一样赋值给变量、存入切片、作为参数和返回值,是实现回调、过滤器和中间件的基础:

package main

import (
	"fmt"
)

// 定义函数类型,签名必须是 func(int) bool
type testInt func(int) bool

func check(a int) bool {
	switch a % 2 {
	case 0:
		return true
	case 1:
		return false
	}
	return false
}

// filter 接收一个函数 f 作为过滤条件
func filter(a []int, f testInt) (b []int) {
	for _, v := range a {
		if f(v) {
			b = append(b, v)
		}
	}
	return
}

func main() {
	a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	odd := filter(a, check)
	// check 对偶数返回 true,因此过滤出的实际是偶数
	fmt.Println(odd) // [0 2 4 6 8 10]
}

泛型 (Generics)

泛型让你在定义函数、类型时预留「类型参数」,从而写出与具体类型解耦、又能保持类型安全的代码。

[T any] 表示声明一个类型参数 Tany 约束 T 可以是任意类型(语义等价于 interface{},但泛型保留了编译期的真实类型)。使用时必须把 T 替换为具体类型,例如 SelectOption[string]SelectOption[int]。这样既避免了 interface{} 的装箱开销和类型断言,又能在编译期校验类型。

// SelectOption 表示下拉框中的单条选项,Label 展示文案,Value 绑定实际值
type SelectOption[T any] struct {
	Label string
	Value T
}

func main() {
	// 用具体类型 string 实例化泛型类型
	options := make([]SelectOption[string], 0, 3)
	options = append(options, SelectOption[string]{
		Label: "Ethereum",
		Value: "0x1",
	})
	options = append(options, SelectOption[string]{
		Label: "Optimism",
		Value: "0x10",
	})

	for _, opt := range options {
		fmt.Printf("label=%s value=%s\n", opt.Label, opt.Value)
	}
}

输出:

label=Ethereum value=0x1
label=Optimism value=0x10

同理,泛型函数也能复用逻辑而无需为每种类型各写一份,例如:

// 泛型函数:返回切片中的第一个元素,适用于任意类型
func first[T any](s []T) (T, bool) {
	var zero T
	if len(s) == 0 {
		return zero, false
	}
	return s[0], true
}

func main() {
	v, ok := first(options)
	fmt.Println(v.Label, ok) // Ethereum true
}

多类型实例化

同一个泛型类型可以在不同地方分别用不同的具体类型实例化。SelectOption[string]SelectOption[int] 在编译期是两个完全不同的类型,彼此独立、互不影响:

func main() {
	// 地方一:string 版选项(链 ID 用字符串标识)
	chainOpts := make([]SelectOption[string], 0, 2)
	chainOpts = append(chainOpts, SelectOption[string]{Label: "Ethereum", Value: "0x1"})
	chainOpts = append(chainOpts, SelectOption[string]{Label: "Optimism", Value: "0x10"})

	// 地方二:int 版选项(区块高度用整数)
	heightOpts := make([]SelectOption[int], 0, 2)
	heightOpts = append(heightOpts, SelectOption[int]{Label: "Block 1", Value: 1})
	heightOpts = append(heightOpts, SelectOption[int]{Label: "Block 100", Value: 100})

	fmt.Println(chainOpts[0].Value)  // 0x1
	fmt.Println(heightOpts[1].Value) // 100
}

需要注意的约束:

  1. 同一切片内类型必须一致[]SelectOption[string] 只能容纳 SelectOption[string],混入 SelectOption[int] 会直接编译报错——两者虽然都由同一个模板生成,但类型系统认为它们无关。
  2. 各自独立。上面 chainOptsheightOpts 可以共存于同一函数、同一包,互不干扰。
  3. 如果确实需要「一个切片装多种类型的值」,应改用 interface{} / any,但这样就失去了泛型带来的编译期类型安全,一般不推荐混用。