基础语法
包
- 每个 Go 程序都由包构成
- 程序从 main 包开始运行
- 按照约定,包名与导入路径的最后一个元素一致
导入
- 单行导入
import "fmt"
- 分组导入
import (
"fmt"
"math"
)
提示
推荐使用分组导入
导出名
导出名必须以大写字母开头, 任何「未导出」的名字在该包外均无法访问。
函数
函数可接受零个或多个参数。
func add(x int, y int) {
return x + y
}
多个函数参数类型相同时,除最后一个参数外,参数类型可以省略。
func add(x, y int) {
return x + y
}
函数支持返回任意个数的返回值
func swap(x, y string) (string, string) {
return y, x
}
函数错误处理
通常,使用最后一个参数表示函数是否有错误产生。
import (
"fmt",
"errors",
"log"
)
func getFullName(fristName, lastName string) (string, error) {
if firstName == ""
return "", errors.New("First name must be not empty")
if lastName == ""
return "", errors.New("Last name must be not empty")
return firstName + lastName
}
func main() {
fullName, err := getFullName("Dog", "Lea")
if err != nil {
log.Fatal(err)
return
}
fmt.Println("fullName: ", fullName)
}
命名函数返回值 函数的返回值可被命名,它们会被视作定义在函数顶部的变量。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
提示
- 返回值的命名应当能反应其含义
- 没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。
- 裸返回语句应只在短函数中使用,在长函数中使用会影响代码可读性。
可变参数函数
可变参函数可以使用任意个数的参数进行函数调用。例如:fmt.Println
func sum(nums ...int) {
fmr.Print(nums, " ")
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
func main() {
nums := []int{1, 2, 3, 4}
sum(nums...)
}
匿名函数
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
func newSequence() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
nextInt := newSequence()
// 每次调用产生一个自增数字
fmt.Println(nextInt())
}
匿名函数也可立即执行
func main() {
func(msg string) {
fmt.Println(msg)
}("going")
}
递归函数
匿名函数和命名函数均支持递归,匿名函数进行递归时,需要使用变量名接收函数地址。
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}
func main() {
fmt.Println(fact(7))
var fib func(n int) int
fib = func(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
fmt.Println(fib(7))
}
init函数
init()函数就像main函数一样,不带任何参数也不返回任何东西。 每个包中都存在此函数,并且在初始化包时将调用此函数。 该函数是隐式声明的,因此不能从任何地方引用它,并且可以在同一程序中创建多个init()函数,并且它们将按照创建顺序执行。 你可以在程序中的任何位置创建init()函数,并且它们以词汇文件名顺序(字母顺序)调用。 并允许在init()函数中放置语句,但始终记住要在main()函数调用之前执行init()函数,因此它不依赖于main()函数。 init()函数的主要目的是初始化无法在全局上下文中初始化的全局变量。
func init() {
fmt.Println("init")
}
func main() {
// 先输出init 后输出hello world
fmt.Println("Hello World")
}
变量
声明变量
- 变量声明可以包含初始值,每个变量对应一个。
- 如果提供了初始值,则类型可以省略;变量会从初始值中推断出类型。
var i, j int = 1, 2
短变量声明
k := 3
注意
短变量声明仅允许在函数内部使用,函数外部每个语句必须以关键字开始。
基本类型
名称 | 字节数 | 零值 | 最大值 | 最小值 | 说明 |
---|---|---|---|---|---|
bool | 1 | false | - | - | 布尔类型,真用常量true表示,假用常量false表示 |
byte | 1 | 0 | -128 | 127 | 字节类型,可看作uint8的别名类型 |
string | - | "" | - | - | 字符串类型(实质是字节序列) |
float32 | 4 | 0.0 | -2147483648 | 2147483647 | 由32位二进制数表示的浮点数类型 |
float64 | 8 | 0 | - | - | 由64位二进制数表示的浮点数类型 |
rune | 4 | 0 | - | - | rune类型,专门存储Unicode编码,可看作uint32的别名类型 |
int | 4/8 | 0 | 32: -232 64: -264 | 32: 232-1 64: 264-1 | 有符号整数类型 32位系统占4字节,64位系统占8字节 |
int8 | 1 | 0 | -128 | 127 | 由8位二进制数表示的有符号整数类型 |
int16 | 2 | 0 | -32768 | 32767 | 由16位二进制数表示的有符号整数类型 |
int32 | 4 | 0 | -232 | 232 | 由32位二进制数表示的有符号整数类型 |
int64 | 8 | 0 | -264 | 264-1 | 由64位二进制数表示的有符号整数类型 |
uint | 4/8 | 0 | 0 | 32: 232-1 64: 264-1 | 无符号整数类型 32位系统占4字节,64位系统占8字节 |
uint8 | 1 | 0 | 0 | 127 | 由8位二进制数表示的无符号整数类型 |
uint16 | 2 | 0 | 0 | 65535 | 由16位二进制数表示的无符号整数类型 |
uint32 | 4 | 0 | 0 | 4294967295 | 由32位二进制数表示的无符号整数类型 |
uint64 | 8 | 0 | 0 | 264 - 1 | 由64位二进制数表示的无符号整数类型 |
uintptr | - | - | - | 无符号整数类型,一个存储指针位模式的整数值。 | |
complex64 | 8 | 0 | - | - | 由64位二进制数表示的复数类型,float32类型的实部和float32类型的虚部联合表示 |
complex128 | 16 | 0 | - | - | 由128位二进制数表示的复数类型,float64类型的实部和float64类型的虚部联合表示 |
类型转换
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
或者使用简洁形式
i := 42
f := float64(i)
u := unit(f)
类型推断
使用已知类型变量为短变量赋值时,短变量类型与赋值的变量类型相同
var i int = 3
// j 也是int类型
j := i
短变量声明时,右侧为数字时,取决于数字的精度
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
类型断言
常量
- 常量的声明与变量类似,只不过使用 const 关键字。
- 常量可以是字符、字符串、布尔值或数值。
- 常量不能用 := 语法声明。
const PI = 3.14
range关键字
range支持遍历数组(切片)、map以及字符串。
遍历数组
func main() { nums := []int{2, 3, 4} for _, num := range nums { fmt.Println("num: ", num) } }
遍历map
kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) }
遍历字符串
for i, c := range "hello world" { // i表示字节数组索引,c表示unicode值 fmt.Println(i, c) }
提示
for ... range
会自动将string拆分为rune数组,而不是字符数组。请参考rune类型。
rune类型
rune用于表示一个unicode码点,本质上是int32。
为什么需要rune?
Go的string是UTF-8字节序列,而一个中文,表情可能是多个字节,用byte处理容易出错。
使用场景
- 字符统计
- 字符串反转
- 截取字符串中的字符
- 自定义解码器或语法分析器
字符串
字符串是一个只读字节片,字符串的字节可以使用UTF-8编码在Unicode文本中表示。字符串采用UTF-8编码,可以包含文本,文本是世界上任何语言的混合,而不会造成页面的混乱和限制。
字符串比较
字符串支持一下两种比较方式:
运算符比较:==,!=,> =,<=,<,>
使用运算符比较时会先判断长度,再判断ascii码值。
func main() { // 长度不同 false fmt.Println("hello" == "hell") // 长度不同 true fmt.Println("hello" != "hell") // "o"的ascii码值大于"i" true fmt.Println("hello" >= "helli") }
Compare函数
函数签名
func Compare(str1, str2 string) int
要使用此函数,需要导入
strings
包。import ( "fmt" "strings" ) func main { // 相等,返回0 fmt.Println(strings.Compare("hello", "hello")) // i的ascii码值小于o 返回-1 fmt.Println(strings.Compare("helli", "hello")) }
字符串拼接
将两个或多个字符串添加到新的单个字符串中的过程称为串联。连接Go语言中两个或多个字符串的最简单方法是使用运算符(+)。也称为串联运算符。
func main() {
s1 := "Bob"
s2 := "Welcom"
res1 = s1 + s2
res2 := "Welcom"
res2 += s1
fmt.Println("res1: ", res1)
fmt.Println("res2: ", res2)
}
也可以使用bytes.Buffer
进行拼接。
import (
"fmt"
"bytes"
)
func main() {
var buf bytes.Buffer
buf.WriteString("B")
buf.WriteString("o")
buf.WriteString("b")
// 输出: Bob
fmt.Println(b.String())
}
字符串修剪
Trim
修剪此函数中指定的所有前导和后缀Unicode代码点的字符串。
func Trim(str string, cutstr string) string
import ( "fmt" "strings" ) func main() { s := "!!Welcome to nhooo !!" // Welcom to nhoo fmt.Println(strings.Trim(s, "!")) }
TrimLeft
修剪字符串的左侧(在函数中指定)Unicode代码点。
func TrimLeft(str string, cutstr string) string
import ( "fmt" "strings" ) func main() { s := "!!Welcome to nhooo **" // Welcom to nhoo ** fmt.Println(strings.Trim(s, "!*")) }
TrimRgiht
修剪字符串的右侧(在函数中指定)Unicode代码点。
func TrimRight(str string, cutstr string) string
import ( "fmt" "strings" ) func main() { s := "!!Welcome to nhooo **" // !!Welcom to nhoo fmt.Println(strings.Trim(s, "!*")) }
TrimSpace
修剪指定字符串中的所有前导和尾随空白(空格)。
func TrimSpace(str string) string
import ( "fmt" "strings" ) func main() { s := "!!Welcome to nhooo **" // !!Welcom to nhoo fmt.Println(strings.Trim(s, "!*")) }
TrimSuffix
用于修剪给定字符串中的尾随后缀字符串。如果给定的字符串不包含指定的后缀字符串,则此函数将返回原始字符串,而不进行任何更改。
func TrimSuffix(str, suffstr string) string
import ( "fmt" "strings" ) func main() { s := "Welcome, nhooo" // Welcom, fmt.Println(strings.TrimSuffix(s, "nhooo")) // Welcome, nhooo fmt.Println(strings.TrimSuffix(s, "hello")) }
TrimPrefix
用于从给定字符串中修剪前导前缀字符串。如果给定的字符串不包含指定的前缀字符串,则此函数将返回原始字符串,而不进行任何更改。
func Trim(str string, cutstr string) string
import ( "fmt" "strings" ) func main() { s := "Welcome, nhooo" // , nhooo fmt.Println(strings.TrimSuffix(s, "Welcom")) // Welcome, nhooo fmt.Println(strings.TrimSuffix(s, "hello")) }
字符串切割
Split
此函数将字符串拆分为由给定分隔符分隔的所有子字符串,并返回包含这些子字符串的切片。
func Split(str, sep string) []string
SplitAfter
此函数在给定分隔符的每个实例之后将字符串拆分为所有子字符串,并返回包含这些子字符串的切片。
func SplitAfter(str, sep string) []string
SplitAfterN
此函数在给定分隔符的每个实例之后将字符串拆分为所有子字符串,并返回包含这些子字符串的切片。
func SplitAfterN(str, sep string, m int) []string
字符串包含
Contains
此函数用于检查给定字符串中是否存在给定字符。如果字符存在于给定的字符串中,则它将返回true,否则返回false。
func Contains(str, chstr string) bool
ContainsAny
此函数用于检查给定字符串(str)中是否存在 charstr 中的任何Unicode字符。如果给定字符串(str)中有 charstr 中的任何Unicode字符,则此方法返回true,否则返回false。
func ContainsAny(str, charstr string) bool
字符串索引
Index
此函数用于从原始字符串中查找给定字符串的第一个实例的索引值。如果给定的字符串在原始字符串中不存在,则此方法将返回-1。
func Index(str, sbstr string) int
IndexAny
此方法从原始字符串中的chars返回任何Unicode码的第一个实例的索引值。如果原始字符中没有来自chars的Unicode代码点,则此方法将返回-1。
func IndexAny(str, charstr string) int
IndexByte
此函数返回原始字符串中给定字节的第一个实例的索引。如果给定的字节在原始字符串中不存在,则此方法将返回-1。
func IndexByte(str string, b byte) int
空白标识符(下划线)
错误
- 错误通常放在最后。
- 使用
errors.New
创建一个错误。 - 没有错误返回
nil
。
import "errors"
func f(arg int) (int, error){
if arg <= 0 {
return -1, errors.New("The value must be greater than 0")
}
return arg + 3, nil
}
自定义错误
type argError struct {
arg int
message string
}
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.message)
}
哨兵错误
哨兵错误是一种声明为全局变量的错误,使用errors.New
创建,通常以Err
开头。
import "erros"
var ErrUnauthorized = errors.New("Not found")
注意
- 哨兵错误是包级别的,可在包外进行错误判断,用于表示那些不需要额外上下文、含义明确的错误条件。
- 使用哨兵错误会造成包和包之间的依赖。如果哨兵错误做了修改,那么所有依赖该错误的包都需要修改。
- go1.13后,如果函数的返回值是哨兵错误,应该对哨兵错误进行包装后再返回,调用时使用errors.Is函数判断是否为某个具体的哨兵错误。
包装错误
使用fmt.Errorf
创建包装错误。
var ErrInvalidNum = errors.New("Invalid num")
func foo(arg int) (int, error){
if arg <= 0 {
return -1, fmt.Errorf("The value must be greater than 0: %w", ErrInvalidNum)
}
return arg + 3, nil
}
判断错误是否匹配
使用errors.Is
判断错误是否包含目标错误,此函数会逐一检查错误链的错误是否等于目标错误值。
val, err := foo(-1)
if errors.Is(err, ErrInvalidNum) {
fmt.Println("Invalid num")
}
错误转换
errors.As
用于将错误专程特定的自定义错误类型(结构体或接口),从而访问错误中的详细信息。
使用场景:
- 自定义错误结构体或接口
- 需要判断某个错误是否属于该类型,并访问字段。
type MyError struct{
Detail string
}
func (err MyError) Error() string {
return fmt.Printf("error: %s", err.Detail)
}
func foo(arg int) (int, error) {
if arg <= 0 {
return nil, &MyError{ "The value must be greater than 0" }
}
return arg + 3
}
func main() {
val, err := foo(-1)
if errors.Is(err, MyError) {
var myErr *MyError
errors.As(err, myErr)
fmt.Println(myErr.Detail)
}
}
总结:
errors.As
会逐层检查错误链(即调用errros.Unwrap()得到的错误链)- 尝试将错误链中的错误实例赋值给用户提供的目标类型指针
- 只要链中符合类型的错误,赋值成功,返回true
循环
Go 只有一种循环结构:for 循环。基本的 for 循环由三部分组成,它们用分号隔开:
- 初始化语句:在第一次迭代前执行
- 条件表达式:在每次迭代前求值
- 后置语句:在每次迭代的结尾执行
初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。一旦条件表达式求值为 false,循环迭代就会终止。
提示
for循环无小括号,大括号是必须的。
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
也可使用range
进行遍历
arr := []{1, 2, 3}
for i := range arr {
fmt.Println(i)
}
初始化语句和后置语句是可选的
func main() {
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
}
for 是 Go 中的「while」
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
func main() {
for {
}
}
分支语句
if判断
- 与 for 循环类似,表达式外无需小括号 ( ),而大括号 { } 则是必须的。
- if 语句可以在条件表达式前执行一个简短语句。
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
- 在 if 的简短语句中声明的变量同样可以在对应的任何 else 块中使用。
switch
仅运行选定之后的case语句,而非所有之后的所有case。
func category(name string) string {
switch name {
case "dog":
case "pig":
case "cat": return "animal"
case "apple":
case "orange":
case "banana": return "fruit"
default: panic("Unkown category")
}
}
func main() {
// 期望返回animal,实际报错
fmt.Println(category("cat"))
}
对于以上情况,可以使用fallthrough
关键字。
func category(name string) string {
switch name {
case "dog": fallthrough
case "pig": fallthrough
case "cat": return "animal"
case "apple": fallthrough
case "orange": fallthrough
case "banana": return "fruit"
default: panic("Unkown category")
}
}
也可以使用逗号分隔表达式
func category(name string) string {
switch name {
case "dog", "pig", "cat": return "animal"
case "apple", "orange", "banana": return "fruit"
default: panic("Unkown category")
}
}
func main() {
// cat
fmt.Println(category("cat"))
}
如果不使用fallthrough
,相当于为每个case语句自动添加break
。 另外,case后的值无需为常量,且取值不限于整数。
无条件switch
无条件的 switch 同 switch true 一样。
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("早上好!")
case t.Hour() < 17:
fmt.Println("下午好!")
default:
fmt.Println("晚上好!")
}
}
数组
切片
迭代器
go从1.23
版本开始支持迭代器语法。
Go的迭代器是range over func
风格,我们可以直接用for range
关键字来进行使用。例如:
func main() {
n := 8
for f := range Fibonacci(n) {
if n >= 10 {
// 停止获取,yeild()函数值为false
break
}
fmt.Println(n)
}
}
func Fibonacci(n int) func(yield func(int) bool) {
a, b, c := 0, 1, 1
return func(yield func(int) bool) {
for range n {
if !yield(a) {
return
}
a, b = b, c
c = a + b
}
}
}
如上所示,迭代器就是一个闭包函数,它接受一个回调函数作为参数,你甚至可以在里面看到yield
这种字眼Go的迭代器并没有新增任何关键字,语法特性,在上述示例中yield
也只是一个回调函数,它并非关键字,官方取这个名字是为了方便理解。而推送式迭代器的重要依据就是:通过调用 yield
函数逐步推出一系列值,yield
函数返回 bool
,决定是否继续执行推出操作。
总结:
- 非关键字:yield不是一个关键字,只是一个普通函数
- 双向通信:既能传递值,又能接收控制信号
- 惰性求值:需要时才产生值
- 资源友好:可以及时停止昂贵的操作
- 控制权交换:每次
yield
调用都是一次控制权转移
函数签名格式
func(yield func() bool)
func(yield func(V) bool)
func(yield func(K, V) bool)
推送式迭代器
推送式迭代器(pushing iterator)是由迭代器来控制迭代的逻辑,用户被动获取元素。
上面的斐波那契数列
函数就是一个推送式迭代器
。
拉取式迭代器
拉取式迭代器(pulling iterator)就是由用户来控制迭代逻辑,主动的去获取序列元素。
拉取式迭代器都会有特定的函数如next()
,stop()
来控制迭代的开始或结束,它可以是一个闭包或者结构体。
package main
import (
"fmt"
"iter"
)
func Fibonacci(n int) func(yield func(int) bool) {
// 同上
}
/*
拉取式迭代器
*/
func main() {
n := 10
next, stop := iter.Pull(Fibonacci(n))
for {
v, ok := next()
if !ok {
break
}
fmt.Println(v)
// stop函数也会导致迭代器终止
stop()
}
}
使用场景
文件读取
func readLines(filename string) iter.Seq[string] { return func(yield func(string) bool) { file, _ := os.Open(filename) defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { if !yield(scanner.Text()) { return // 消费者不需要更多行了 } } } } // 只读取前10行 for line := range readLines("bigfile.txt") { fmt.Println(line) if lineCount >= 10 { break // 及时停止读取,节省资源 } lineCount++ }
数据库查询
func queryUsers() iter.Seq[User] { return func(yield func(User) bool) { rows, _ := db.Query("SELECT * FROM users") defer rows.Close() for rows.Next() { var user User rows.Scan(&user) if !yield(user) { return } } } }
迭代器标准库
slices.All
将切片转换成一个切片迭代器
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]
slices.Values
将切片转换成一个切片迭代器,但是不带索引
func Values[Slice ~[]E, E any](s Slice) iter.Seq[E]
slices.Chunk
返回一个迭代器,该迭代器会以n个元素为切片推送给调用者
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]
slices.Collect
将切片迭代器收集成一个切片
func Collect[E any](seq iter.Seq[E]) []E
maps.Keys
会返回一个迭代map所有键的迭代器,配合
slices.Collect
可以直接收集成一个切片。func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]
maps.Values
会返回一个迭代map所有值的迭代器,配合
slices.Collect
可以直接收集成一个切片。func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V]
maps.All
将一个map转换为成一个map迭代器
func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V]
maps.Collect
可以将一个map迭代器收集成一个map
func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]V
函数
泛型
泛型参数
func sum[K comparable, V int | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
ints := map[string]int64{
"first": 34,
"second": 12,
}
sum[string, int](ints)
也可以省略泛型参数进行函数调用,编译器会自动推断范型V
为int
类型。
sum(ints)
提示
如果函数没有参数,不能省略类型参数。
泛型类型
类型可以使用类型参数进行参数化,这对于实现通用数据结构非常有用。
type List[T any] struct {
next *List[T]
val T
}
指针
指针存储的是变量在内存中的地址值。
使用指针访问或修改其所指向实际值的过程称为解引用,又称间接引用 。
在go
中,使用*
定义一个指针
var a = 1
var b *int = &a
&
操作符表示获取变量a
的地址。
获取指针类型变量存储的值,可以使用*
fmt.Println(*b);
结构体
结构体就是一组字段
type Vetex struct {
X int
Y int
}
上面的结构体可以通过点号.
访问
v := Vertext{1, 2}
fmt.Println(v.X)
定义结构体
// 方式一
v := Vertext{1, 2}
// 方式2: 命名字段
v := Vertext{X: 1, Y: 2}
// 方式3: 指定部分字段,未指定字段使用字段类型零值
// Y的类型是int,int类型零值为0
v := Vertext{X: 1}
// 方式4: 结构体指针
v := &Vertext{1, 2}
创建对象更常见的做法是使用构造函数
func newVertex(x, y int) *Vertext {
v := Vertext{}
v.X = 1
v.Y = 2
// 返回结构体指针
return &v
}
结构体指针
结构体字段可通过结构体指针来访问。
v := Vertex{1, 2}
p := &v
// 等价于 (*p).X = 1e9
p.X = 1e9
(&v).Y = 3
嵌套结构体
ype base struct {
num int
}
func (b base) describe() string {
return fmt.Sprintf("base with num=%v", b.num)
}
type container struct {
base
str string
}
func main() {
co := container{
base: base{
num: 1,
},
str: "some name",
}
// 直接访问
fmt.Println(co.num)
// 使用全路径访问
fmt.Println(co.base.num)
type describer interface {
describe() string
}
// co定义了describe()方法,与desriber类型describe方法定义相似,称为隐式转换
var d describer = co
fmt.Println("describer:", d.describe())
}
匿名结构体
匿名结构体必须使用变量进行接收
// 全局
// 属性值均为空
var person = struct {
name string
age int
}{}
// 在函数中定义
func test() {
// 指定字段名初始化
p1 := struct {
name string
age int
}{
name: "张三",
age: 18
}
// 按顺序初始化所有字段
p2 := struct {
name string
age int
}{
"张三",
18
}
// 按顺序初始化部分字段,编译报错
p3 := struct {
name string
age int
} {
"张三"
}
}
提示
匿名结构体按顺序初始化时,必须初始化所有字段。
结构体方法
结构体支持定义方法。
type rect struct {
width, height int
}
// 使用指针接收者
func (r *rect) area() int {
return r.width * r.height
}
// 使用值作为接收者
func (r rect) perim() int {
return 2*r.width + 2*r.height
}
接口
并发
defer 推迟
defer 语句会将函数推迟到外层函数返回之后执行。 推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。
使用场景:
- 释放资源