Skip to content

基础语法

6323字约21分钟

2025-09-26

  • 每个 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
}

提示

  1. 返回值的命名应当能反应其含义
  2. 没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。
  3. 裸返回语句应只在短函数中使用,在长函数中使用会影响代码可读性。

可变参数函数

可变参函数可以使用任意个数的参数进行函数调用。例如: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

注意

短变量声明仅允许在函数内部使用,函数外部每个语句必须以关键字开始。

基本类型

名称字节数零值最大值最小值说明
bool1false--布尔类型,真用常量true表示,假用常量false表示
byte10-128127字节类型,可看作uint8的别名类型
string-""--字符串类型(实质是字节序列)
float3240.0-21474836482147483647由32位二进制数表示的浮点数类型
float6480--由64位二进制数表示的浮点数类型
rune40--rune类型,专门存储Unicode编码,可看作uint32的别名类型
int4/8032: -232
64: -264
32: 232-1
64: 264-1
有符号整数类型
32位系统占4字节,64位系统占8字节
int810-128127由8位二进制数表示的有符号整数类型
int1620-3276832767由16位二进制数表示的有符号整数类型
int3240-232232由32位二进制数表示的有符号整数类型
int6480-264264-1由64位二进制数表示的有符号整数类型
uint4/80032: 232-1
64: 264-1
无符号整数类型
32位系统占4字节,64位系统占8字节
uint8100127由8位二进制数表示的无符号整数类型
uint1620065535由16位二进制数表示的无符号整数类型
uint324004294967295由32位二进制数表示的无符号整数类型
uint64800264 - 1由64位二进制数表示的无符号整数类型
uintptr---无符号整数类型,一个存储指针位模式的整数值。
complex6480--由64位二进制数表示的复数类型,float32类型的实部和float32类型的虚部联合表示
complex128160--由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以及字符串。

  1. 遍历数组

    func main() {
      nums := []int{2, 3, 4}
      for _, num := range nums {
        fmt.Println("num: ", num)
      }
    }
  2. 遍历map

    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
      fmt.Printf("%s -> %s\n", k, v)
    }
  3. 遍历字符串

    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编码,可以包含文本,文本是世界上任何语言的混合,而不会造成页面的混乱和限制。

字符串比较

字符串支持一下两种比较方式:

  1. 运算符比较:==,!=,> =,<=,<,>

    使用运算符比较时会先判断长度,再判断ascii码值。

    func main() {
      // 长度不同 false
      fmt.Println("hello" == "hell")
      // 长度不同 true
      fmt.Println("hello" != "hell")
      // "o"的ascii码值大于"i" true
      fmt.Println("hello" >= "helli")
    }
  2. 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())
}

字符串修剪

  1. 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, "!"))
    }
  2. 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, "!*"))
    }
  3. 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, "!*"))
    }
  4. TrimSpace

    修剪指定字符串中的所有前导和尾随空白(空格)。

    func TrimSpace(str string) string
    import (
      "fmt"
      "strings"
    )
    func main() {
      s := "!!Welcome to nhooo **"
      // !!Welcom to nhoo
      fmt.Println(strings.Trim(s, "!*"))
    }
  5. 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"))
    }
  6. 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"))
    }

字符串切割

  1. Split

    此函数将字符串拆分为由给定分隔符分隔的所有子字符串,并返回包含这些子字符串的切片。

    func Split(str, sep string) []string
  2. SplitAfter

    此函数在给定分隔符的每个实例之后将字符串拆分为所有子字符串,并返回包含这些子字符串的切片。

    func SplitAfter(str, sep string) []string
  3. SplitAfterN

    此函数在给定分隔符的每个实例之后将字符串拆分为所有子字符串,并返回包含这些子字符串的切片。

    func SplitAfterN(str, sep string, m int) []string

字符串包含

  1. Contains

    此函数用于检查给定字符串中是否存在给定字符。如果字符存在于给定的字符串中,则它将返回true,否则返回false。

    func Contains(str, chstr string) bool
  2. ContainsAny

    此函数用于检查给定字符串(str)中是否存在 charstr 中的任何Unicode字符。如果给定字符串(str)中有 charstr 中的任何Unicode字符,则此方法返回true,否则返回false。

    func ContainsAny(str, charstr string) bool

字符串索引

  1. Index

    此函数用于从原始字符串中查找给定字符串的第一个实例的索引值。如果给定的字符串在原始字符串中不存在,则此方法将返回-1。

    func Index(str, sbstr string) int
  2. IndexAny

    此方法从原始字符串中的chars返回任何Unicode码的第一个实例的索引值。如果原始字符中没有来自chars的Unicode代码点,则此方法将返回-1。

    func IndexAny(str, charstr string) int
  3. IndexByte

    此函数返回原始字符串中给定字节的第一个实例的索引。如果给定的字节在原始字符串中不存在,则此方法将返回-1。

    func IndexByte(str string, b byte) int

空白标识符(下划线)

错误

  1. 错误通常放在最后。
  2. 使用errors.New创建一个错误。
  3. 没有错误返回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")

注意

  1. 哨兵错误是包级别的,可在包外进行错误判断,用于表示那些不需要额外上下文、含义明确的错误条件。
  2. 使用哨兵错误会造成包和包之间的依赖。如果哨兵错误做了修改,那么所有依赖该错误的包都需要修改。
  3. 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判断

  1. 与 for 循环类似,表达式外无需小括号 ( ),而大括号 { } 则是必须的。
  2. if 语句可以在条件表达式前执行一个简短语句。
func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim
}
  1. 在 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()
	}
}

使用场景

  1. 文件读取

    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++
    }
  2. 数据库查询

    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
                }
            }
        }
    }

迭代器标准库

  1. slices.All

    将切片转换成一个切片迭代器

    func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]
  2. slices.Values

    将切片转换成一个切片迭代器,但是不带索引

    func Values[Slice ~[]E, E any](s Slice) iter.Seq[E]
  3. slices.Chunk

    返回一个迭代器,该迭代器会以n个元素为切片推送给调用者

    func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]
  4. slices.Collect

    将切片迭代器收集成一个切片

    func Collect[E any](seq iter.Seq[E]) []E
  5. maps.Keys

    会返回一个迭代map所有键的迭代器,配合slices.Collect可以直接收集成一个切片。

    func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]
  6. maps.Values

    会返回一个迭代map所有值的迭代器,配合slices.Collect可以直接收集成一个切片。

    func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V]
  7. maps.All

    将一个map转换为成一个map迭代器

    func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V]
  8. 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)

也可以省略泛型参数进行函数调用,编译器会自动推断范型Vint类型。

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 语句会将函数推迟到外层函数返回之后执行。 推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。

使用场景:

  1. 释放资源

协程

信道