包和工具
在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。
例如,math/rand包的每个源文件的开头都包含package rand包声明语句,所以当你导入这个包,你就可以用rand.Int、rand.Float64类似的方式访问包的成员。s
通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。
Go 语言规定:同一个目录下的所有 Go 文件必须属于同一个包🔥🔥🔥。
如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。
1 | import ( |
导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为path的变量,那么我们可以将“path”标准包重命名为pathpkg。
每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。
在 Go 的工程实践中,包(Package)分为两类:
| 类型 | 声明方式 | 用途 |
|---|---|---|
| 可执行包 | package main |
编译后产生 .exe (Windows) 或 二进制文件 (Linux),可以直接运行。 |
| 依赖包/库 | package <任意名称> |
不能直接运行,只能被其他程序通过 import 引用,提供功能支持。 |
程序结构
声明与变量
在 Go 语言中,字面量(Literal) 的概念和 C++ 中的字面量非常相似,都是指在代码中直接表示固定值的符号或表达式。它们用于表示基本数据类型(如整数、浮点数、字符、字符串等)的常量值,而无需通过变量或计算来获取。
| 场景 | 短赋值 := |
一般赋值 = |
|---|---|---|
| 函数内声明新变量 | ✅ | ❌(需配合 var) |
| 包作用域变量赋值 | ❌ | ✅ |
| 仅赋值(变量已声明) | ❌ | ✅ |
| 多变量赋值(含新变量) | ✅ | ❌ |
| 显式指定变量类型 | ❌ | ✅(配合 var) |
流程控制
for
Go 只有一种循环结构:for 循环。基本的 for 循环由三部分组成,它们用分号隔开,表达式外无需小括号 ( ),而大括号 { } 则是必须的
1 | for i := 0; i < 10; i++ { |
初始化语句和后置语句是可选的,且可以去掉分号,此时for相当于C++中的while
1 | for sum < 1000 { |
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑 for { }
for 循环的 range 形式
-
每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本🔥🔥🔥
-
可以遍历 silce 以及 map,string应该也行
也是融合了c++两种for循环写法,索引遍历和迭代器遍历
可以将下标或值赋予 _ 来忽略它。
1 | for i, _ := range pow |
if else
Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ),而大括号 { } 则是必须的
-
和
for一样,if语句可以在条件表达式前执行一个简短语句,该语句声明的变量作用域仅在if之内。
1 | if v := math.Pow(x, n); v < lim { |
在 if 的简短语句中声明的变量同样可以在对应的任何 else 块中使用。if 和 else 的使用方式除去小括号外与C++一致
swtich case
switch 语句是编写一连串 if - else 语句的简便方法。它运行第一个 case 值 值等于条件表达式的子句。
Go 只会运行选定的 case,而非之后所有的 case。 在效果上,Go 的做法相当于这些语言中为每个 case 后面自动添加了所需的 break 语句。 在 Go 中,除非以 fallthrough 语句结束,否则分支会自动终止。
-
case无需为常量,且取值不限于整数 -
case语句从上到下顺次执行,直到匹配成功时停止 -
无条件的
switch同switch true一样,该形式能将一长串if-then-else写得更加清晰。
1 | func main() { |
🔥defer推迟
defer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但只会在外层函数返回前该函数才会被调用
-
推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。
基础数据类型
字符
Unicode建立二进制编码与字符的映射,UTF-8是一个将Unicode码点编码为字节序列的变长编码,下图中x的部分是Unicode定义的字符二进制编码,每个字节中显式定义的字节前缀就是UTF编码格式
1 | 0xxxxxxx runes 0-127 (ASCII) |
在 Go 语言中,rune 是一个内置的数据类型别名,它实际上是 int32 的别名。
得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:
-
解码: 特指将 UTF-8 字节序列 -> 转换为 Unicode 码点序列 (
[]byte->[]rune)。
每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串🔥🔥🔥。
字符串
内置的 len 函数可以返回一个字符串中的字节数目(不是 rune 字符数目),索引操作 s [ i ] 返回第 i 个字节的字节值,i必须满足 0 ≤ i < len( s ) 条件约束。
-
反引号用于原生字符串字面量,里面的内容不会做任何转义处理,写什么就是什么,适合包含特殊字符或多行字符串。(注:在原生字符串面值内部是无法直接写
字符的,可以用八进制或十六进制转义或+""连接字符串常量完成) -
双引号用于解释型字符串字面量,里面的内容会做转义处理,比如
"\n"会被解释为换行。
原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。
字符串包
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。
-
strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
-
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
-
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
-
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
字符串和数字的转换
由strconv包提供这类转换功能
将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”):
1 | x := 123 |
FormatInt和FormatUint函数可以用不同的进制来格式化数字:
1 | fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011" |
mt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候:
1 | s := fmt.Sprintf("x=%b", x) // "x=1111011" |
如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数:
1 | x, err := strconv.Atoi("123") // x is an int |
ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。
有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。
复合数据类型
指针
一如C语言,其保存了值的内存地址
类型 *T 是指向 T 类型值的指针,其零值为 nil。
1 | var p *int |
& 操作符会生成一个指向其操作数的指针。
1 | i := 42 |
* 操作符表示指针指向的底层值。
1 | fmt.Println(*p) // 通过指针 p 读取 i |
这也就是通常所说的「解引用」或「间接引用」。
与 C 不同,Go 无法对指针进行四则运算,即直接修改地址,能够保障内存的安全性
结构体
一个 结构体(struct)就是一组 字段(field)
1 | type Vertex struct { |
结构体指针
如果我们有一个指向结构体的指针 p 那么可以通过 (*p).X 来访问其字段 X。 不过这么写太啰嗦了,所以语言也允许我们使用隐式解引用,直接写 p.X 就可以。
数组
类型 [n]T 表示一个数组,它拥有 n 个类型为 T 的值。
1 | var a [10]int |
会将变量 a 声明为拥有 10 个整数的数组。
-
数组的长度是其类型的一部分,因此数组不能改变大小。
🔥切片
类型 []T 表示一个元素类型为 T 的切片。.
切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔:
1 | a[low : high] |
它会选出一个半闭半开区间,包括第一个元素,但排除最后一个元素。
-
切片就像数组的引用,切片并不存储任何数据,它只是映射了底层数组,更改切片的元素会修改其底层数组中对应的元素
-
切片下界的默认值为 0,上界则是该切片的长度
切片拥有 长度 和 容量。
-
切片的长度就是它所包含的元素个数
-
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
-
切片
s的长度和容量可通过表达式len(s)和cap(s)来获取
切片的零值是 nil
-
nil 切片的长度和容量为 0 且没有底层数组
切片可以用内置函数 make 来创建,这也是你创建动态数组的方式。
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:
1 | a := make([]int, 5) // len(a)=5 |
要指定它的容量,需向 make 传入第三个参数:
1 | b := make([]int, 0, 5) // len(b)=0, cap(b)=5 |
切片可以包含任何类型,当然也包括其他切片,可以使用切片嵌套来创建更高维的数据结构
Go 提供了内置的 append 函数为切片追加新的元素,内置函数的文档对该函数有详细的介绍。
1 | func append(s []T, vs ...T) []T |
append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾
-
当
s的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组。真的像vector吧
1 | var s []int |
复合字面量命名
在 out := []int{} 这行代码里,大括号 {} 是复合字面量(composite literal)的语法,用来表示一个空的切片字面量。
拆开看:
-
[]int是类型:元素为int的切片。 -
{}是值:一个空的元素列表。
所以 out := []int{} 的意思是:声明变量 out,类型是 []int,并把它初始化为一个长度为 0、容量为 0 的空切片(但它不是 nil 切片)。
1 | var out []int // out == nil,长度 0,容量 0 |
两者长度都是 0,都能直接 append,但有一个关键区别:[]int{} 是非 nil 的空切片。
这会影响:
-
JSON 序列化:
nil切片序列化成null,空切片序列化成[]。如果你希望接口返回[]而不是null,就应该用[]int{}。 -
和 nil 比较:
out == nil对[]int{}是false。 -
反射/测试断言:有些测试会区分 nil 和空切片。
map映射
map 映射将键映射到值。
-
映射的零值为
nil。nil映射既没有键,也不能添加键。
1 | var m map[string]int |
声明一个key为string类型,value为int类型的map,此时的map没有引用任何哈希表,属于零值
1 | m := make(map[string]int) |
声明一个key为rune类型,value为[]string 类型的map
1 | m := make(map[rune][]string) |
短赋值一个key为string类型,value为int类型的map
映射的字面量和结构体类似,只不过必须有键名,若顶层类型只是一个类型名,那么你可以在字面量的元素中省略它。
修改映射
在映射 m 中插入或修改元素:
1 | m[key] = elem |
获取元素:
1 | elem = m[key] |
删除元素:
1 | delete(m, key) |
通过双赋值检测某个键是否存在(这种形式可以在不希望因为查找操作自动插入新元素时使用):
1 | elem, ok = m[key] |
若 key 在 m 中,ok 为 true ;否则,ok 为 false。
若 key 不在映射中,则 elem 是该映射元素类型的零值。
注:若 elem 或 ok 还未声明,你可以使用短变量声明:
1 | elem, ok := m[key] |
函数
函数也是值
-
它们可以像其他值一样传递。
-
函数值可以用作函数的参数或返回值。
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。 该函数可以访问并赋予其引用的变量值,换句话说,该函数被“绑定”到了这些变量。
1 | func compute(fn func(float64, float64) float64) float64 { |
函数闭包🔥🔥🔥
1 | func adder() func(int) int { |
pos 和 neg 这两个变量,都是通过 adder() 函数创建的闭包
-
adder() 函数返回一个函数 func(int) int
即pos和neg变量都可以视作为一个函数,并且具有一个外部变量sum:
1 | func(x int) int { |
它们引用的闭包中的 sum 变量跟随 main 函数的生命周期,因此每次调用pos和neg时会在上一次加和的基础上进行计算
复合字面量
复合字面量(composite literal)是 Go 里构造值的核心语法
1 | Point{X: 1, Y: 2} // struct,带字段名 |
嵌套省略类型是个高频技巧:[]Point{{1,2},{3,4}} 里内层 Point 可省,因为外层已经确定了元素类型。这在二维结构里很省字。
结构体
字段名可以换行、顺序随意
1 | p := Point{ |
只写部分字段 → 其余是零值
1 | type Config struct { |
空字面量 T{} 及 new(T)
1 | p := Point{} // 值,零值 |
-
T{}得到值。 -
&T{}得到指针,是 Go 里"构造并取地址"的惯用法,比new更常见(尤其要初始化字段时:&Point{X:1})。 -
new(T)只能得到零值指针,不能设字段,所以有初始值时一律用&T{...}。
切片/映射
预知容量时配 make,而不是纯字面量
1 | s := make([]int, 0, 100) // 预分配,避免多次扩容 |
字面量 []int{...} 适合元素已知且固定;动态填充用 make + append 更高效。
二维切片可以省内层 []T
1 | grid := [][]int{ |
map 字面量的尾逗号规则
1 | m := map[string]int{ |
方法与接口
方法
Go 没有类。不过你可以为类型定义方法。
-
方法就是一类带特殊的 接收者 参数的函数。
-
方法接收者在它自己的参数列表内,位于
func关键字和方法名之间。
在此例中,Abs 方法拥有一个名字为 v,类型为 Vertex 的接收者
1 | type Vertex struct { |
方法只是个带接收者参数的函数,上下两段代码等效
1 | type Vertex struct { |
也可以为非结构体类型声明方法,但接收者的类型定义和方法声明必须在同一包内
接收者参数需要在函数内再进行一次赋值才能进行迭代,若是直接对接收者参数操作则只会产生一个新的局部变量,无法真正影响到接收者参数的值
1 | type List[T any] struct { |
接口
接口类型 的定义为一组方法签名,接口类型的变量可以持有任何实现了这些方法的值。
接口也是值。它们可以像其它值一样传递。
-
接口值可以用作函数的参数或返回值。
-
在内部,接口值可以看做包含值和具体类型的元组:
1 | (value, type) |
-
接口值保存了一个具体底层类型的具体值。
-
接口值调用方法时会执行其底层类型的同名方法。
接口的本质:
-
接口就像是一个"合同"或"协议"
-
它定义了一组方法,但只声明这些方法应该做什么,而不具体实现
-
任何类型只要实现了接口中定义的所有方法,就自动实现了这个接口
即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用, 保存了 nil 具体值的接口其自身并不为 nil。
nil 接口值(即仅声明不赋值)既不保存值也不保存具体类型,为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
指定了零个方法的接口值被称为空接口:
1 | interface{} |
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
类型断言
类型断言 提供了访问接口值底层具体值的方式。
1 | t := i.(T) |
该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。
若 i 并未保存 T 类型的值,该语句就会触发一个 panic。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
1 | t, ok := i.(T) |
若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生 panic。
Stringer
1 | type Stringer interface { |
Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。
1 | func (p Person) String() string { |
可以直接通过Stringer 自定义输出类型
错误与处理
Go 程序使用 error 值来表示错误状态。
与 fmt.Stringer 类似,error 类型是一个内建接口:
1 | type error interface { |
-
error 是一个接口,它要求实现者必须提供一个 Error() string 方法。
-
任何类型只要实现了 Error() string 方法,就可以作为 error 类型使用。
(与 fmt.Stringer 类似,fmt 包也会根据对 error 的实现来打印值。)
通常函数会返回一个 error 值,调用它的代码应当判断这个错误是否等于 nil 来进行错误处理。
1 | i, err := strconv.Atoi("42") |
error 为 nil 时表示成功;非 nil 的 error 表示失败。
检查与解包:errors.Is 与 errors.As
不要使用 == 直接比较错误,也不要直接使用类型断言,因为包装后的错误会破坏这些操作。
-
errors.Is(err, target):判断错误链中是否包含特定的“哨兵错误”(Sentinel Error)。它取代了err == target。例如,检查文件是否存在:errors.Is(err, os.ErrNotExist)。 -
errors.As(err, &target):判断错误链中是否包含特定类型的错误,并将其赋值给目标变量。它取代了err.(*MyError)这样的类型断言。
创建与包装错误
在实际应用中,我们很少直接返回裸的错误信息,而是需要携带上下文。
基础创建与自定义类型
-
errors.New:创建一个只包含静态文本的错误。 -
自定义错误类型:通过定义结构体并实现
Error()方法,可以携带结构化的数据(如错误码、具体参数等)。随后可以使用errors.As将其从错误链中提取出来。
包装错误(Wrapping)
为了给错误添加调用上下文(例如“读取配置失败”),同时又不丢失原始错误,Go 1.13 引入了 %w 动词。
1 | if err != nil { |
使用 %w 包装后,底层错误被“隐藏”在链中,但仍然可以被 errors.Is 和 errors.As 发现。如果使用 %v,则只会拼接文本,丢失错误链。
多错误合并
从 Go 1.20 开始,errors.Join 可以将多个独立的错误合并成一个。这在并发任务或清理操作中非常有用,且合并后的错误依然支持 errors.Is/As 检查。
数据读写
Readers
io 包指定了 io.Reader 接口,它表示数据流的读取端。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader 接口有一个 Read 方法:
1 | func (T) Read(b []byte) (n int, err error) |
b []byte 表示Read输入数据所使用的buffer,返回的 int 为buffer长度(填充的字节数),error 为错误值,在遇到数据流的结尾时,它会返回一个 io.EOF 错误。
示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。
嵌套Reader
1 | type rot13Reader struct { |
一个实现了 io.Reader 并从另一个 io.Reader 中读取数据的 rot13Reader,通过应用 rot13 代换密码对数据流进行修改。
并发
Go 程(goroutine)
由 Go 运行时管理的轻量级线程,go后跟的函数的参数会在go语句自身执行时被求值🔥🔥🔥。
1 | go f(x, y, z) |
会启动一个新的 Go 协程并执行
1 | f(x, y, z) |
f, x, y 和 z 的求值发生在当前的 Go 协程中,而 f 的执行发生在新的 Go 协程中。
下列代码存在一个常见的 goroutine 与循环变量捕获的问题
1 | for _, f := range filenames { // f 是单个变量,每次迭代被重用 |
-
f是单个变量,在每次迭代中被重用(内存地址不变,只是值改变) -
闭包捕获的是变量
f本身,而不是迭代时的值 -
goroutine 执行时,
f可能已经指向下一个值
每个迭代启动一个 goroutine,但 goroutine 不会立即执行,可能会导致所有go程只会执行最后一个文件
信道
信道是带有类型的管道,元素先进先出,你可以通过它用信道操作符 <- 来发送或者接收值。
1 | ch <- v // 将 v 发送至信道 ch。 |
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:
1 | ch := make(chan int) |
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
带缓冲的信道
信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:
1 | ch := make(chan int, 100) |
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。缓冲区若填满会产生死锁
从 channel 持续接收值,直到 channel 被关闭
1 | for v := range ch |
大致等价于
1 | for { |
所以 range ch 的关键点是:它会在 channel 关闭后自动退出,不需要手动判断 ok。
sync.WaitGroup
| 方法 | 作用 |
|---|---|
Add(delta int) |
计数器 += delta,在启动 goroutine 前调用 |
Done() |
计数器 -= 1,通常 defer wg.Done() |
Wait() |
阻塞直到计数器归 0 |
标准模板
1 | var wg sync.WaitGroup |
关键规则(踩坑点)
-
Add必须在启动 goroutine 之前,不能放进 goroutine 内部,否则Wait可能提前返回(竞争)。 -
Add的增量要和Done次数匹配。Done多于Add→panic: sync: negative WaitGroup counter。 -
WaitGroup不可拷贝(含sync.noCopy),传参用指针*sync.WaitGroup。 -
Wait之后可以复用:计数器归 0 后,可以再次Add开始新一轮(但不要和正在进行的Wait混用同一轮)。 -
协调者不参与计数:像"等所有 worker 完成再 close(ch)"的那个 goroutine,只
wg.Wait(),不要wg.Done()。
sync.Mutex / sync.RWMutex
1 | var mu sync.Mutex |
-
用
defer mu.Unlock()防漏解锁。 -
优先"不共享":把累加等写操作收拢到单 goroutine,就不用锁(channel 通信优于共享内存)。
泛型
泛型函数
可以使用类型参数编写 Go 函数来处理多种类型。 函数的类型参数出现在函数参数之前的方括号之间。
1 | func Index[T comparable](s []T, x T) int |
此声明意味着 s 是满足内置约束 comparable 的任何类型 T 的切片。 x 也是相同类型的值。
comparable 是一个有用的约束,它能让我们对任意满足该类型的值使用 == 和 != 运算符。在此示例中,我们使用它将值与所有切片元素进行比较,直到找到匹配项。 该 Index 函数适用于任何支持比较的类型。
泛型类型
除了泛型函数之外,Go 还支持泛型类型。 类型可以使用类型参数进行参数化,这对于实现通用数据结构非常有用。
Go函数轮子
字符串
1 | import "strconv" //字符串操作包 |
其他
Go 没有 public / private 关键字,而是用首字母大小写决定可见性:
-
TaskId、TaskType、FileName→ 导出,包外可见 -
taskId、taskType、fileName→ 包私有,包外不可见