泛型 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/* 不会忽略隐藏文件
-
-
注意相同文件嵌入不同变量的资源副本问题(浪费)
-
过大的文件会导致二进制程序过大,注意性能问题