Go 学习笔记

基本语法

注释

/*
 *  行间注释
 */

// 行内注释

变量

var a int                    // 定义一个整型变量
var b, c bool = true, false  // 定义两个布尔变量并初始化

// 变量声明的分组样式,factored style
var (
  ToBe   bool       = false
  MaxInt uint64     = 1<<64 - 1
  z      complex128 = complx.Sqrt(-5 + 12i)
)

func main() {
  k := 3  // 短声明,函数内可以使用
}

注意,:= 是声明符,不是赋值符

数组和切片

var a [10]int  // 长度为 10 的整型数组,其类型为 [10]int
a[0] = 1       // 数组元素赋值

primes := [6]int{2, 3, 5, 7, 11, 13}  // 数组字面量

var s []int = primes[1:4]  // 切片类似 Python 的数组引用,前闭后开。类型为 []T

s := []int{2, 3}  // 切片字面量
s = append(s, 5)  // 向切片添加元素

a := make([]int, 5)  // 可以使用 make 构造切片

map

var m map[string]int      // 声明一个 string: int 的 map
m = make(map[string]int)  // 可以用 make 创建 map 实例

// 也可以定义 map 字面量
var m = map[string]int{
  "math_score": 150,
  "phy_score": 110,
}

// 如果顶级类型仅是一个类型名称,则可以从字面量的元素中省略它
var m = map[string]Vertex{
  "Bell Labs": {40.68433, -74.39967},
  "Google":    {37.42202, -122.08408},
}

// map 操作
m[key]              // 插入/取出/更新元素
delete(m, key)      // 删除元素
elem, ok = m[key]   // 检测 key 是否在 map 中。如果在,则 ok == true
elem, ok := m[key]  // 如果 elem, ok 还没有被声明,你可以使用这种短声明形式

函数

// 一个基本函数
func add(x, y int) int {
  return x + y
}

// 返回多个值的函数
func swap(x, y string) (string, string) {
  return y, x
}
a, b := swap("world", "hello")  // 接收函数返回值

函数闭包

函数闭包是一个捕获了外部变量的函数。

func adder() func(int) int {  // 声明一个返回值为函数的函数
  sum := 0  // 属于 adder() 的变量
  return func(x int) int {
    sum += x  // 捕获了属于 adder() 的 sum 变量
    return sum
  }
}

每次调用 adder() 时都返回一个闭包,这个闭包和本次调用的 adder() 内的 sum 绑定

指针

var p *int  // p 是一个指向 int 的指针
p = &a      // 取地址
*p = 2      // 解引用

分支逻辑

循环

Go 只有 for 循环,没有 while 循环。

// 一个基本的 for 循环
for i := 0; i < 10; i++ {
  sum += i
}

// 范围循环
for i, v := range arr {
  fmt.Printf("index: %d, value: v\n", i, v)
}

// 可以忽略 v 值
for i := range arr {
  fmt.Printf("index: %d\n", i)
}

// 也可以忽略 i 值,但要用 _ 指出
for _, v := range arr {
  fmt.Printf("value: %d\n", v)
}

// for 其实就是 go 语言的 while
for sum < 1000 {
  sum += sum
}

// 无限循环
for {
  sum += 1
}

if 和 switch

if x < 0 {
  sum += 1
} else {
  sum -= 1
}

// if 可以带一个短表达式
if v := math.Pow(x, n); v < lim {  // 只有首字母大写的名字会被导出,因此是 Pow
  return v
}

// switch,不需要 break
switch os := runtime.GOOS; os {
  case "darwin":
    fmt.Println("OS X.")
  case "linux":
    fmt.Println("Linux.")
  default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.\n", os)
}

结构体和方法

结构体

Go 的结构体相当于 C++ 中的类

type Vertex struct {
  X int  // 在 Go 中,首字母大写的变量/函数等是 public 的,首字母小写的是 private 的
  Y int
}

func main() {
  v := Vertex{1, 2}  // 定义一个结构体字面量
  v.X = 4            // 使用结构体

  p := &v    // p 是结构体的指针
  p.X = 1e9  // Go 可以隐式解引用

  v2 := Vertex{X: 1}  // 可以只为指定成员变量赋值
}

type 关键字的特殊用法:你可以将基本数据类型包装为自定义类型。

type MyFloat float64  // 定义 MyFloat 为 float64
type IPAddr [4]byte   // 定义 IP 地址类型为 4 字节的数组

方法

Go 语言没有类,但是你可以为类型指定方法。

方法,相当于 C++ 中类的成员函数

// 通过在函数名前面加上 receiver 参数列表来将函数变为方法
func (v Vertex) Abs() float64 {  // (v Vertex) 表示该方法的操作对象是类型为 Vertex 的变量 v
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 由于方法经常用来改变 receiver 内部的值,所以 receiver 指针更为常用
func (v *Vertex) Scale(f float64) {
  v.X = v.X * f  // 改变了 v 内部的值
  v.Y = v.Y * f
}

var v Vertex  // 声明一个 Vertex 变量
v.Scale       // Scale() 方法的操作对象本来应该是指向 Vertex 的指针,但是 go 进行了指针间接访问,把 v.Scale 解释为了 (&v).Scale
p = &v        // 声明一个指向 Vertex 的指针
p.Abs()       // 反过来也是,Abs() 方法的操作对象本来应该是 Vertex 变量,但是却提供了指针。go 将 p.Abs 解释为了 (*p).Abs

接口

接口是方法签名的集合

type Abser interface {
  Abs() float64  // 接口方法
}

// 任何实现了接口方法的类型都隐式实现了接口
func (f MyFloat) Abs() float64 {  // MyFloat 实现了 Abser 接口,那么 MyFloat 就是一种 Abser
  ...
}

func (v *Vertex) Abs() float64 {  // Vertex 实现了 Abser 接口
  // 可以这样优雅地处理“空指针”引用
  if t == nil {
    fmt.Println("<nil>")
    return
}

// 接口变量可以保存任何实现了接口方法的结构体的实例
var a Abser  // nil,一个零值接口既不持有值也不持有具体类型
a = MyFloat(-math.Sqrt(2))  // 可以赋值为 MyFloat
a = &Vertex{3, 4}           // 也可以赋值为 Vertex 指针

// 空接口 interface{} 被用于处理未知类型值的代码。例如,fmt.Print 接受任意数量的 interface{} 类型参数
// 在 Go 1.18 中,interface{} 被 any 类型代替
func describe(i interface{}) {
  fmt.Printf("(%v, %T)\n", i, i)
}

// 等价于
func describe(i any) {
  fmt.Printf("(%v, %T)\n", i, i)
}

// 类型断言
f, ok := i.(float64)  // 如果接口 i 的底层是 float64,那么 f = float64,否则 ok = false

// 类型 switch
switch v := i.(type) {
case T:
    // 在这里 v 的类型为 T
case S:
    // 在这里 v 的类型为 S
default:
    // 都不匹配的情况; 在这里 v 的类型和 i 相同
}

// 最常见的接口:fmt 包定义的 Stringer 接口
type Stringer interface {
  String() string
}

func (p Person) String() string {
  return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

错误

Go 程序用 error 值表示错误状态。

error 类型是一个内置接口,类似于 fmt.Stringer

type error interface {
  Error() string
}

函数通常会返回一个 error 值,调用代码应通过测试 error 是否等于 nil 来处理错误。

i, err := strconv.Atoi("42")
if err != nil {
  fmt.Printf("couldn't convert number: %v\n", err)
  return
}
fmt.Println("Converted integer:", i)
// 实现一个 error 结构体
type MyError struct {
  When time.Time
  What string
}

// 实现 error 接口
func (e *MyError) Error() string {
  return fmt.Sprintf("at %v, %s", e.When, e.What)
}

协程

Go 协程(goroutines)相当于 C++ 中的线程。

func foo() {  // 协程函数
  ...
}

go foo()  // 启动协程

通道

通道是一种类型化的管道,通过它你可以使用通道操作符 <- 发送和接收值。

默认情况下,发送和接收操作会阻塞,直到另一方准备就绪。这使得 goroutine 可以在没有显式锁或条件变量的情况下进行同步。

ch := make(chan int)  // 可以使用 map 创建通道
ch <- v               // 将 v 发送到通道 ch
v := <-ch            // 从通道 ch 接收,并赋值给 v

// 声明接收通道参数的函数
func sum(s []int, c chan int) {
  sum := 0
  for _, v := range s {
    sum += v
  }
  c <- sum  // 将 sum 发送给通道 c
}

func main() {
  s := []int{7, 2, 8, -9, 4, 0}

  c := make(chan int)
  sum(s[:len(s)/2], c)
  x := <-c  // 从通道 c 接收。main 函数在这里等待 sum 函数返回

  fmt.Println(x)
}

带有缓冲区的通道:

ch := make(chan int, 100)  // 创建一个缓冲区长度为 100 的通道

当缓冲区已满时,发送操作会被阻塞。当缓冲区为空时,接收操作会被阻塞。

关闭通道:

close(ch)      // 发送方可以关闭通道以指示不会再发送更多值
v, ok := <-ch  // 接收方可以接收第二个参数来测试通道是否已关闭
func fibonacci(n int, c chan int) {
  x, y := 0, 1
  for i := 0; i < n; i++ {
    c <- x  // 发送一个计算结果
    x, y = y, x+y
  }
  close(c)  // 关闭通道以表示计算结束
}

func main() {
  c := make(chan int, 10)
  go fibonacci(cap(c), c)
  // 循环会重复地从通道接收值,直到通道被关闭。
  for i := range c {
    fmt.Println(i)
  }
}

select 语句允许一个 goroutine 等待多个通信操作。

一个 select 阻塞,直到其中一个 case 可以运行,然后执行该 case。如果有多个 case 准备就绪,则随机选择一个。

func fibonacci(c, quit chan int) {
  x, y := 0, 1
  for {
    select {
    case c <- x:  // 向 c 发送
      x, y = y, x+y
    case <-quit:  // 从 quit 接收
      fmt.Println("quit")
      return
    }
  }
}

func main() {
  c := make(chan int)  // 创建通道
  quit := make(chan int)
  go func() {  // 为字面量函数启动协程
    for i := 0; i < 10; i++ {
      fmt.Println(<-c)  // 从 c 取数据
    }
      quit <- 0  // 停止阻塞 quit
  }()
  fibonacci(c, quit)
}

如果没有其他 case 准备就绪,那么运行 default

select {
case i := <-c:
    // use i
default:
    // receiving from c would block
}

互斥锁

package main

import (
  "fmt"
  "sync"
  "time"
)

// SafeCounter 可以安全地并发使用。
type SafeCounter struct {
  mu sync.Mutex
  v  map[string]int
}

// Inc 对给定 key 的计数器加 1
func (c *SafeCounter) Inc(key string) {
  c.mu.Lock()
  // 获取锁,以便一次只有一个 goroutine 可以访问 map c.v
  c.v[key]++
  c.mu.Unlock()
}

// Value 返回给定 key 的计数器的当前值
func (c *SafeCounter) Value(key string) int {
  c.mu.Lock()
  // 获取锁,以便一次只有一个 goroutine 可以访问 map c.v
  defer c.mu.Unlock()  // 使用 defer 确保在 return 之后再释放锁
  return c.v[key]
}

func main() {
  c := SafeCounter{v: make(map[string]int)}  // 这里只对 map v 赋值
  for i := 0; i < 1000; i++ {
    go c.Inc("somekey")
  }

  time.Sleep(time.Second)
  fmt.Println(c.Value("somekey"))
}

同步锁

可以使用 sync.WaitGroup 来管理并等待所有的协程完成。

var wg sync.WaitGroup  // 创建一个同步锁
for _, u := range urls {
  wg.Add(1)  // 同步锁信号量 +1
  go Crawl(u, depth-1, fetcher, c, &wg)  // 启动一个协程
}
subWg.Wait()  // 等待所有协程结束

defer 关键字

一个 defer 语句会延迟函数的执行,直到其所在的函数返回。

func main() {
  defer fmt.Println("world")  // 在 main 函数 return 之前才执行
  fmt.Println("hello")
}

包管理

package main

// 导入单个包
import "fmt"

// 导入多个包
import (
    "fmt"
    "math"
)

import f "fmt"  // 给导入的包 fmt 起别名 f

import _ "fmt"  // 导入包但不使用(仅用于初始化包)

import "myproject/mypackage"      // 导入本地包

import "github.com/user/package"  // 导入远程包

一些注意事项:

  • 导入的包路径要使用双引号括起来。
  • 导入的包名通常与包的目录名相同,但也可以在包内使用 package 关键字指定不同的包名。
  • 如果导入了包但未使用,编译器会报错。可以使用空白标识符 _ 来导入包但不使用其内容,仅用于初始化该包。
  • 包的导入路径可以是相对路径(本地包)或绝对路径(远程包)。
  • 一个 Go 源文件必须在其 package 声明之后立即导入所需的包。

参考:A Tour of Go

posted @ 2024-08-18 00:00  Undefined443  阅读(15)  评论(0编辑  收藏  举报