Go语言语法更新

泛型 Generic Type

泛型函数和泛型类型。

使用了类型参数的函数或类型称为泛型函数或泛型类型。

类型参数 Type Parameter

Go中通过类型参数来支持泛型的。类型参数需要配合类型和函数的定义来一起使用。

相关概念:

  • 类型形参,Type parameter,定义时的类型参数

  • 类型约束,Type Constraint,定义类型参数时对参数做的类型上的约束。类型约束其实就是接口interface。

  • 实例化,Instantiations,使用类型实参替换类型形参而得到具体类型的过程称为实例化。

  • 类型实参,Type Argument,实例化时传递的实际参数

使用场景:

  • 泛型类型,generic type,类型定义中带有类型参数的,称为泛型类型。

  • 泛型接收器,接收器类型为泛型类型的,称为泛型接收器。

  • 泛型函数,generic function,函数定义中带有类型参数的,称为泛型函数。

  • 泛型接口,接口定义中带有类型参数的,称为泛型接口。

泛型类型 Generic Type

类型定义中带有类型参数的,称为泛型类型。

泛型类型定义示例:

 // 支持特定元素类型的切片
 type mySlice[T int | string] []T // mySlice[T]
 ​
 // 支持特定Key,Value类型的map
 type myMap[K int | string, V float32 | float64] map[K]V // myMap[K, V]
 ​
 // 支持特定字段类型的struct
 type myList[T int | float64] struct { // myList[T]
     data []T
     l    int
     max, min, avg T
 }

泛型类型实例化示例:

 intSlice := mySlice[int]{}
 stringSlice := mySlice[string]{}
 fmt.Printf("%T, %T\n", intSlice, stringSlice)
 ​
 intKeyMap := myMap[int, float64]{}
 stringKeyMap := myMap[string, float32]{}
 fmt.Printf("%T, %T\n", intKeyMap, stringKeyMap)

结果:

 >go test -run=TP
 ref.mySlice[int], ref.mySlice[string]
 ref.myMap[int,float64], ref.myMap[string,float32]
 PASS

以 myMap为例,说明相关概念:

 // 类型myMap[K, V]
 // 类型参数(形参)列表:[K int | string, V float32 | float64]
 // K 的类型约束: int | string
 // V 的类型约束: float32 | float64
 // 基础类型:map[K]V
 type myMap[K int | string, V float32 | float64] map[K]V
 ​
 // 实例化
 // 类型参数实参:string, float32
 stringKeyMap := myMap[string, float32]{
 "go":    98,
 "mysql": 88.5,
 }

类型约束支持指定基础类型

类型约束中主要使用 | 表示或。

使用~T表示,以T为基础类型的类型都支持。

示例:

 type intS[T ~int] []T
 type myInt int
 type yourInt myInt
 // 以下三个都是支持的
 var a intS[int]
 var b intS[myInt]
 var c intS[yourInt]
 ​
 // int32 does not implement ~int (int32 missing in ~int)
 var d intS[int32]

基于泛型定义类型

泛型类型可以作为其他泛型类型的基础类型来定义,示例:

 type Slice[T int | string | float32 | float64] []T
 // 用在类型定义时
 type FloatSlice[T float32 | float64] Slice[T]
 ​
 // 用在结构体字段中
 type myStruct[T float32 | float64] struct {
     F1 Slice[T]
 }
 ​
 // 用在map中
 type myMap[T float32 | float64] map[string]Slice[T]

泛型类型语法注意事项

  • 不能直接使用类型参数作为类型声明的RHS使用

  • 匿名结构体不支持类型参数

  • 类型约束会导致语法歧义的要使用:构成合法的表达式就会导致语法歧义

    • 逗号结尾

    • interface{}包裹

示例:

 // Cannot use a type parameter as RHS in type declaration
 type mySlice[T int | string] T
 ​
 // 不支持
 [T int|string]struct{ //struct[T int|string]{
     name T
 }{
     name: "Go"
 }
 ​
 // 语法歧义
 type T[P *int] []P // P * int 乘法
 type T[P (int)] []P // P(int) 函数调用
 type T[P *int|string] []P // P*int|string, P乘以int位或string
 // Invalid array bound 'T*int | *string', must be a constant expression
 type pointSlice[T *int|*string] []T
 // 解决方案
 type pointSlice[T *int | *string,] []T
 type pointSlice[T interface{ *int | *string }] []T

泛型接收器

接收器类型为泛型类型的接收器,称为泛型接收器。也就是泛型类型的方法。

带有泛型接收器的方法,可以在参数、返回值位置使用类型参数,来提高方法的通用性。

并发的slice操作示例代码:

 // 一:定义泛型类型
 type myList[T int | float64] struct {
     data     []T
     max, min T
     m        sync.Mutex
 }
 ​
 // 二:定义泛型类型的方法集
 // 1, 添加元素,更新最大值最小值
 // 泛型接收器, 泛型类型参数类型
 func (l *myList[T]) Add(ele T) *myList[T] {
     // 加锁并发安全考虑
     l.m.Lock()
     defer l.m.Unlock()
 ​
     // 更新 data
     l.data = append(l.data, ele)
 ​
     // 统计 min max
     if len(l.data) == 1 {
         l.max = ele
         l.min = ele
     }
 ​
     if ele > l.max {
         l.max = ele
     }
 ​
     if ele < l.min {
         l.min = ele
     }
 ​
     return l
 }
 ​
 // 2, 获取元素
 func (l myList[T]) All() []T {
     return l.data
 }
 ​
 func (l myList[T]) Max() T {
     return l.max
 }
 ​
 func (l myList[T]) Min() T {
     return l.min
 }
 ​
 func GenericReceiver() {
     l := myList[int]{}
     // 添加
     l.Add(1).Add(3).Add(10)
     // 获取元素
     fmt.Println(l.All())
     fmt.Println(l.Max(), l.Min())
 }

练习:

定义队列,支持多种类型的push,pop操作。使用泛型实现。

示例代码:

type Queue[T int | string] struct {
    data []T
}

func (q *Queue[T]) Put(v ...T) *Queue[T] {
    q.data = append(q.data, v...)
    return q
}

func (q *Queue[T]) Pop() (T, bool) {
    var v T
    if len(q.data) == 0 {
        return v, true
    }

    v = q.data[0]
    q.data = q.data[1:]
    return v, len(q.data) == 0
}

func (q Queue[T]) Size() int {
    return len(q.data)
}

泛型函数

带有类型参数的函数,称为泛型函数。可以让一段函数代码,具备同时处理多种相似类型的能力。

示例:

func Sum[T int | string](ele ...T) T {
    var s T
    for _, v := range ele {
        s += v
    }
    return s
}

func GenericFunc() {
    fmt.Println(Sum[int](1, 2, 3))
    fmt.Println(Sum[string]("ma", "shi", "bing"))
}

没有匿名泛型函数,不能使用类型参数,来实现匿名泛型函数。

但是,可以在匿名函数内,使用所处函数的类型参数:

示例:

func Sum[T int | string](ele ...T) T {
    var s T
    for _, v := range ele {
        s += v
    }
    // 匿名函数可以使用所处的函数中定义的类型参数
    func(n T) {
	}(s)
    return s
}

泛型函数的类型推断,Type Inference

缺少的类型参数的泛型函数可以通过参数判定类型,为了方便泛型函数的调用:

func GuessType[T int | string](ele ...T) T {
	var s T
	for _, v := range ele {
		s += v
	}
	return s
}

func Inference() {
	fmt.Println(GuessType(1, 2, 3))
	fmt.Println(GuessType("ma", "shi", "bing"))
}

测试:

> go test -run=Inference -v
=== RUN   TestInference
6
mashibing
--- PASS: TestInference (0.00s)
PASS
ok      github.com/han-joker/goExample/generalType      0.038s

类型推断在使用时,支持省略全部或部分类型参数,例如:

func GuessType2[K int | string, V float64 | string](p1 K, p2 V) {
}
// 全部指定
GuessType2[int, string](42, "Ma")
// 指定前面部分
GuessType2[int](42, "Ma")
// 指定后边部分
GuessType2(42, "Ma")

推断的类型必须要合理,例如:

func GuessType3[K int | string, V []K](p1 K, p2 V) {
}
// 合理
GuessType3(42, []int{})
// 不合理
GuessType3(42, []string{})

泛型接口

使用类类型参数的接口,称为泛型接口。

示例:

type Data[T int | string] interface {
	Process(T) (T, error)
	Save(data T) error
}

以上泛型接口,在使用时,同样需要传递具体实参类型,才有意义,示例:

func DataOperate(d Data[string]) {

}
// 
Data[string]
相当于
type Data interface {
	Process(string) (string, error)
	Save(data string) error
}

类型在实现接口时,必须要实现 Data[string] 才可以,例如:

type JsonData struct{}

func (JsonData) Process(string) (string, error) {
	return "", nil
}

func (JsonData) Save(string) error {
	return nil
}

type NumberData struct{}

func (NumberData) Process(int) (int, error) {
	return 0, nil
}

func (NumberData) Save(int) error {
	return nil
}

func GenerateInterface() {
	// 参数类型正确
	DataOperate(JsonData{})
	// 参数类型错误
	DataOperate(NumberData{})
}

泛型接口的目的同样是不同类型相似的接口,可以通过定义一个泛型接口来实现。

接口

方法集更新为类型集

1.18前,接口的定义为:

An interface type specifies a method set called its interface

1.18后,接口的定义为:

An interface type defines a *type set*

大家注意,由 method set 变化为 type set。

示例:

// 方法集合
type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Write(p []byte) (n int, err error)
}

// 类型集合
type Float interface {
	~float32 | ~float64
}

混合了方法和类型定义的接口:

// 混合
type FloatReadWriter interface {
	~float32 | ~float64
	Read(p []byte) (n int, err error)
	Write(p []byte) (n int, err error)
}

接口中元素的逻辑关系

接口中的元素,存在并集和交集的关系

  • 并集:| 表示并集,实现其中一个元素即可

  • 交集:行之间表示交集,全部行都要实现

因此以上3种接口语法分别表示:

// 方法集合
// 要同时实现 Read 和 Write
type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Write(p []byte) (n int, err error)
}

// 类型集合
// 要实现 float32 或 float64 , ~表示以T为基础类型
type Float interface {
	~float32 | ~float64
}

// 混合
// 要实现 float32 或 float64,同时实现Read和Write
type FloatReadWriter interface {
	~float32 | ~float64
	Read(p []byte) (n int, err error)
	Write(p []byte) (n int, err error)
}

实现示例:

// 方法集合实现
type rw struct{}

func (rw) Read(p []byte) (n int, err error) {
	return
}
func (rw) Write(p []byte) (n int, err error) {
	return
}


type myFloat float32
type yourFloat float64

// 混合实现
func (myFloat) Read(p []byte) (n int, err error) {
	return
}
func (myFloat) Write(p []byte) (n int, err error) {
	return
}

空集

行之间为交集关系,那就意味着可能出现空集,例如:

type Empty interface {
    int
    string 
}

这种空集接口编译可以通过,但使用上没实际意义。

注意,空集接口与空接口interface{}是不同的,两个极端:

  • 空集接口,不会有任何类型实现

  • 空接口interface{},全部类型都实现了这个接口

使用接口

仅包含方法集的接口,称为基本接口,与之前接口使用方式保持一致。

包含了类型集的接口,称为一般接口,只能用在泛型的类型约束中。

示例以上三个接口的使用:

func TypeSet() {
	ms(rw{})

	ts[myFloat]()
	ts[yourFloat]()

	mix[myFloat]()
	//mix[yourFloat]()
}

func ms(p ReadWriter)         {}
func ts[T Float]()            {}
func mix[T FloatReadWriter]() {}

类型约束本质是接口

类型参数的类型约束,其实就是接口。因此当出现语义冲突时可以使用:

// 语法歧义
type T[P *int] []P // P * int 乘法
type T[P (int)] []P // P(int) 函数调用
type T[P *int|string] []P // P*int|string, P乘以int位或string
// Invalid array bound 'T*int | *string', must be a constant expression
type pointSlice[T *int|*string] []T
// 解决方案
type pointSlice[T *int | *string,] []T
type pointSlice[T interface{ *int | *string }] []T

预定义的接口的使用

interface{}和any

interface{} 表示任何类型的集合,可以使用关键字 any表示。

any 的定义:

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

有需要的话,命令:

gofmt -w -r 'interface{} -> any' ./...

可以将 interface{} 全部更新为 any。

comparable 可比较

定义:

// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

可比较指的是可以执行 ==, != 运算。包括:

  • booleans

  • numbers

  • strings

  • pointers

  • 元素类型为可比较的 channel和arrays

  • 全部字段都为可比较的structs

典型的,我们要求map类型的key就是可比较的。

type myMap[K comparable, V any] map[K]V

若定义成:

type myMap[K, V any] map[K]V

就是错误的。

注意:可比较是比较是否相等。不保证比较大小。也就是comparable 不保证支持 >,<,<=,>= 运算。

ordered 可排序

支持 >,<,<=,>= 运算的类型,典型使用如下:

type Ordered interface {
    Integer | Float | ~string
}

type Integer interface {
    Signed | Unsigned
}

type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Float interface {
    ~float32 | ~float64
}

注意,不是go的内置类型。但通常会使用。

Number 数值

type Integer interface {
	Signed | Unsigned
}
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Float interface {
	~float32 | ~float64
}
type Number interface {
	Float | Integer
}

模糊测试 Fuzzing

go test 支持单元测试和基准测试,1.18是增加了模糊测试。

单元测试

单元测试,unit test,主要用以测试函数是否完成某项功能。示例如下:

功能代码示例:

func Add(a, b uint8) uint8 {
	return a + b
}

单元测试代码:

# _test.go
func TestAdd(t *testing.T) {
	type item struct {
		a, b, s uint8
	}
	items := []item{
		{1, 1, 2},
		{10, 56, 66},
		{100, 101, 201},
		{123, 1, 124},
	}

	for _, v := range items {
		s := Add(v.a, v.b)
		if s != v.s {
			t.Errorf("a: %d, b: %d, s: %d", v.a, v.b, s)
		}
	}
}

运行单元测试:

> go test -run TestAdd -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      github.com/han-joker/goExample/generalType      0.029s

单元测试的局限性:

测试用例完全需要用户编写,若想完成用例覆盖,则需要大量的测试用例编写。

模糊测试

Go 1.18在go工具链里引入了fuzzing模糊测试,可以帮助我们发现Go代码里的漏洞或者可能导致程序崩溃的输入。

模糊测试的核心特征,是可以自动随机生成大量的测试用例,用于保证覆盖大量的数据场景。

使用方法

语法特点:

  • 位于*_test.go 中

  • 测试函数以Fuzz开头

  • 测试函数参数为 *testing.F类型

*testing.F的典型方法:

  • f.Add(),将特定数据作为种子加入到测试用例中,种子数据类型要与测试数据函数的类型一致

  • f.Fuzz(),用于完成模糊测试,需要提供测试函数,典型的测试函数以*testing.T为第一个参数,测试用例数据为后续参数

func (f *F) Fuzz(ff any)

运行模糊测试:

  • go test,会自动执行Test和Fuzz开头的单元和模糊测试

  • go test -run Pattern,仅执行匹配Pattern的测试

  • go test -fuzz Pattern, 仅执行匹配Pattern的Fuzz。Fuzz就是随机生成测试模糊数据。不带-fuzz不会模糊测试

  • go test --fuzztime 3s,指定fuzz测试时间,默认会一直执行到错误发生

示例:

// Add 的模糊测试
func FuzzAdd(f *testing.F) {
	// 一,添加测试用例种子(到语料库中)
	// 要与测试函数Add保持一致,值随意
	f.Add(uint8(2), uint8(3))

	// 额外:将生成的模糊测试用例,写入到文件中
	file, _ := os.OpenFile("./fuzz_input.txt", os.O_CREATE|os.O_TRUNC, 0644)
	defer file.Close()
	// 二,执行测试,基于模糊数据完成测试
	f.Fuzz(func(t *testing.T, a, b uint8) {
		// 额外,记录下a,b的值
		fmt.Fprintf(file, "a:%d, b:%d\n", a, b)

		// 实现测试
		s := Add(a, b)
		// 模糊测试,由于数据随机生成,因此结果也是随机的
		// 必须要找到可以判定是否结果正确的方法。
		if s-a != b {
			t.Errorf("a: %d, b: %d, s: %d", a, b, s)
		}
	})
}

注意:不同于单元测试,模糊测试的数据用例是随机的,因此编写模糊测试我们要找到办法来判定是否成功。例如本例中,我么可以选择用减法进行运算。互相可逆的算法,通常用来再模糊测试中检测结果是否正确。

  • 加减

  • 乘除

  • 反转,两次反转

  • 布尔值的两次取反运算

上面代码中的./data.txt 文件,与模糊测试无关,是我们用来了解生产的的大量随机数据的。

执行模糊测试:

> go test -run FuzzAdd -fuzz FuzzAdd -v
=== RUN   FuzzAdd
fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed
fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 137820 (45936/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 6s, execs: 304346 (55479/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 9s, execs: 539816 (78408/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 12s, execs: 757555 (72595/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 15s, execs: 971940 (71480/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 18s, execs: 1177823 (68413/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 21s, execs: 1370681 (64399/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 24s, execs: 1554033 (61244/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 27s, execs: 1731988 (59167/sec), new interesting: 1 (total: 5)
fuzz: elapsed: 30s, execs: 1888973 (53294/sec), new interesting: 1 (total: 5)
--- PASS: FuzzAdd (29.96s)
=== NAME
PASS
ok      goSyntax        30.011s

错误处理

更新FuzzAdd,模拟一个错误。

模拟a+b溢出uint8的情景。由于我们之前的单元测试数据没有覆盖可能溢出的场景,我们在模糊测试中,检测这一场景:

if int(s) != int(a)+int(b) {
    t.Errorf("a: %d, b: %d, s: %d", int(a), int(b), int(s))
}

思路就是转换为范围更大的整数,再进行比较。

执行测试:

> go test -run FuzzAdd -fuzz FuzzAdd -v --fuzztime 3s
=== RUN   FuzzAdd
fuzz: elapsed: 0s, gathering baseline coverage: 0/8 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/8 completed
--- FAIL: FuzzAdd (0.06s)
    --- FAIL: FuzzAdd (0.00s)
        fuzzing_test.go:53: a: 101, b: 193, s: 38

    Failing input written to testdata\fuzz\FuzzAdd\b57d10201eb5ef68
    To re-run:
    go test -run=FuzzAdd/b57d10201eb5ef68
=== NAME
FAIL
exit status 1
FAIL    goSyntax        0.097s

错误的用例会被记录到:testdata/fuzz/FuzzAdd/ 目录下的文件中。

示例:第一行为版本,后边的每行为一个测试参数,本例中a,b两个参数,因此形成两行:

go test fuzz v1
byte(',')
byte('Ü')

注意,该错误数据会被保留,下次执行FuzzAdd测试时,会自动使用,即使不使用-fuzz参数。这么做的目的是继续检测错误数据是否可以通过。

当发现问题后,修复问题即可。

> go test -run FuzzAdd -v
=== RUN   FuzzAdd
=== RUN   FuzzAdd/seed#0
=== RUN   FuzzAdd/b57d10201eb5ef68
--- PASS: FuzzAdd (0.00s)
    --- PASS: FuzzAdd/seed#0 (0.00s)
    --- PASS: FuzzAdd/b57d10201eb5ef68 (0.00s)
PASS
ok      goSyntax        0.041s

模糊测试用例种子

种子的意思,会基于种子数据,生成大量的随机数据。以字符串为例:

func FuzzSeed(f *testing.F) {
	f.Add("mashibing")
	file, _ := os.OpenFile("./dataStr.txt", os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
	defer file.Close()
	f.Fuzz(func(t *testing.T, s string) {
		fmt.Fprintf(file, "%s\n", s)
	})
}

执行fuzz:

go test -run FuzzSeed -fuzz FuzzSeed -v

查看datStr.txt文件,检索mashibing,类似的内容如下:

mashibing��+~�I��g�=
mashigina��b
   �ibing
mashibg
mashibing��+~�I��____g�=

标准包 embed

go 1.16

通过引入embed包,在代码中使用//go:embed指令,可以将静态文件编译进Go的二进制执行文件中。该特性的功能:

  • 保证程序的完整性,将程序需要依赖的全部资源打包到一个二进制程序中

  • 易于部署,程序需要的资源全部在一个二进制文件中,各种部署都变得简单

  • 常用的资源访问,在编译时搞定,省去运行时IO的开销,提升效率

场景的案例:

  • 静态web服务器

  • 后端模板载入

  • 常用静态资源响应

导入包

import _ "embed"
import "embed"

嵌入指令

//go:embed Pattern

Pattern是文件路径通配符,支持以下通配符:

通配符释义
?代表任意一个字符(不包括半角中括号)
*代表0至多个任意字符组成的字符串(不包括半角中括号)
[...]和[!...]代表任意一个匹配方括号里字符的字符,!表示任意不匹配方括号中字符的字符
[a-z]、[0-9]代表匹配a-z任意一个字符的字符或是0-9中的任意一个数字
**部分系统支持,*不能跨目录匹配,** 可以。当前与*同义。

Pattern,从项目根目录开始,通常不需要使用/,目录分隔符使用/。路径是文件,仅嵌入文件内容,路径是目录,嵌入目录下的全部文件包括递归子目录。

//go:embed 指令只能用于package范围,不用用于函数范围。

嵌入程序的数据类型

支持三种类型:

  • []byte,任何文件内容,适合单文件。

  • string,字符串文件内容,不适合二进制文件(例如图片,声音),适合单文件。

  • embed.FS,文件系统,适合嵌入目录

单文件嵌入示例

示例代码:

import (
	_ "embed"
	"fmt"
)

//go:embed file/robots.txt
var robots string

//go:embed file/logo.png
var logo []byte

func EmbedFile() {
	fmt.Println(robots)
	fmt.Println(logo)
}

file/robots.txt

User-agent: Baiduspider
Disallow:
User-agent: *
Disallow: /

file/logo.png

测试:

func TestEmbedFile(t *testing.T) {
	EmbedFile()
}

> go test -run EmbedFile
User-agent: Baiduspider
Disallow:
User-agent: *
Disallow: /
[137 80 78 71 .....]
PASS
ok      github.com/han-joker/goExample/em       0.032s

目录嵌入示例

目录必须使用embed.FS类型嵌入。

示例:

// 嵌入目录
//
//go:embed files
var files embed.FS

func EmbedDir() {
	// 获取目录下的全部文件
	entries, err := files.ReadDir("files")
	if err != nil {
		log.Fatal(err)
	}

	// 以此输出每个文件的信息
	for _, entry := range entries {
		info, _ := entry.Info()
		fmt.Println(entry.Name(), entry.IsDir(), info.Size())
	}

	// 读取文件内容
	content, _ := files.ReadFile("files/robots.txt")
	fmt.Println(string(content))
}

测试:

func TestEmbedDir(t *testing.T) {
	EmbedDir()
}

> go test -run EmbedDir
logo.png false 45586
robots.txt false 64
User-agent: Baiduspider
Disallow:
User-agent: *
Disallow: /

PASS
ok      goSyntax        0.030s

embed.FS支持的方法

type FS
// 打开文件
func (f FS) Open(name string) (fs.File, error)
// 读取目录
func (f FS) ReadDir(name string) ([]fs.DirEntry, error)
// 读取文件
func (f FS) ReadFile(name string) ([]byte, error)

示例:静态http服务器

static内容:

/index.html
/img/
	logo.png
/css/
	style.css
/js/
	script.js

嵌入静态文件目录,启动http服务器,等待请求:

//go:embed static
var static embed.FS

// 启动服务器
func StaticEmbedServer() {
	// 获取嵌入的static子目录作为文件系统
	staticFS, _ := fs.Sub(static, "static")
	// 基于static的FS,创建 http.FS
	// 基于http.FS,创建 http.FileServer
	// 启动监听 :8080
	http.ListenAndServe(":8080", http.FileServer(http.FS(staticFS)))
}
func TestStaticServer(t *testing.T) {
	StaticServer()
}

> go test -run StaticServer

浏览器请求:

localhost:8080

运行中我们可以任意更改static中的内容,不影响静态服务器。

作为对比,我们在做一个运行时,读取静态文件内容的http服务器

代码:

// 非嵌入,运行时读取静态文件的服务器
func StaticRuntimeServer() {
	// os.DirFS 基于操作系统的目录文件系统
	staticFS := os.DirFS("static")
	http.ListenAndServe(":8081", http.FileServer(http.FS(staticFS)))
}
func TestStaticRuntimeServer(t *testing.T) {
	StaticRuntimeServer()
}

> go test -run StaticRuntimeServer

浏览器请求:

localhost:8081

运行中我们更新static内容,发现访问404错误了。

语法细节

  • 一定要导入embed包

  • 会自动忽略版本控制目录:

    • .git

    • .svn

    • .bzr

    • .hg

    • .idea不会被忽略

  • dir 和 dir/* 有差异,常用的dir

    • dir 可以嵌入空目录

    • dir/* 不会嵌入空目录

    • dir 会忽略隐藏文件

    • dir/* 不会忽略隐藏文件

  • 注意相同文件嵌入不同变量的资源副本问题(浪费)

  • 过大的文件会导致二进制程序过大,注意性能问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Choice~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值