后端实践:Go语言入门基础语法和常用特性 | 青训营

124 阅读6分钟

听了Go语言入门的课,受益匪浅,也跟着自己实践了很多代码,在此记录一些自认为很有帮助的知识点。

变量

详解

Go 语言的变量必须在使用之前声明,而且编译器会检查变量的类型的正确性。你可以使用 var 关键字声明一个或多个变量,或使用 := 快速赋值。

技巧

  • 在函数体内,可以使用 := 快速定义和初始化一个变量,省略了变量类型。
  • 变量如果没有明确的初始化,则会被赋予其类型的零值。

样例代码

package main

import "fmt"

func main() {

    var a = "initial"  // 声明一个变量
    fmt.Println(a)

    var b, c int = 1, 2  // 声明多个变量,Go会自动推断已有初始值的变量的类型
    fmt.Println(b, c)

    var d = true
    fmt.Println(d)

    var e int  // 声明但不赋值的变量会被初始化为零值
    fmt.Println(e)

    f := "short"  // 短变量声明,只能在函数内部使用
    fmt.Println(f)
}

输出

initial
1 2
true
0
short

解释:此代码段演示了 Go 语言中的变量声明和初始化。var 关键字用于声明变量,可以声明一个或多个变量,也可以显式声明变量的类型。未初始化的变量会被赋予其类型的零值。你还可以使用短变量声明(:=)在函数内部快速声明和初始化变量。

循环

详解

Go 仅支持 for 循环结构,没有 whiledo while 循环。for 可以用于传统的基于计数的循环,也可以用于范围(range)迭代。

技巧

  • for 循环可以不带条件,这种情况下,循环会无限持续下去,可以与 breakreturn 结合使用来中断循环。

样例代码

package main

import "fmt"

func main() {
    // 基本的 for 循环
    for i := 0; i < 3; i++ {
        fmt.Println(i)
    }

    // 类似于 while 循环的 for 循环
    j := 0
    for j < 3 {
        fmt.Println(j)
        j++
    }

    // 无限循环
    k := 0
    for {
        if k >= 3 {
            break
        }
        fmt.Println(k)
        k++
    }
}

输出

0
1
2
0
1
2
0
1
2

解释:Go 中的 for 循环有多种形式。可以使用三个组件(初始化、条件、后置语句)的经典形式,也可以省略其中一些部分来模拟 while 循环或无限循环。

If Else 分支

详解

Go 的 if 语句不需要括号包围条件表达式,但是需要花括号包围分支体。if 语句可以有一个可选的初始化语句。

技巧

  • 可以在 if 表达式之前执行一条简单的语句,例如赋值。

样例代码

package main

import "fmt"

func main() {
    if 7%2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }

    if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
    }

    if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}

输出

7 is odd
8 is divisible by 4
9 has 1 digit

解释:可以使用 if-else 结构创建条件分支。还可以在 if 条件语句之前加入一个简短的声明语句,并在后续的条件中使用该变量。

Switch 分支结构

详解

Go 的 switch 语句比其他主要编程语言更灵活。表达式不必是常量或整数,执行的顺序从上到下,所以可以不用每一个 case 之后都写 break

技巧

  • case 体内的代码从上到下自动中断,不需要 break
  • 不提供 switch 之后的表达式会匹配真值。

样例代码

package main

import (
    "fmt"
    "time"
)

func main() {
    switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a weekday")
    }

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }
}

输出

It's a weekday (如果当前不是周末)
It's before noon 或 It's after noon (取决于当前时间)

解释:switch 语句可以用于多路分支。它可以匹配多个值,也可以省略表达式来创建一种多个 if-else 分支的效果。

数组

详解

在 Go 中,数组是具有相同类型元素的固定长度的序列。

技巧

  • 在 Go 中,如果你想要动态数组,通常使用切片(slice),而不是数组。

样例代码

package main

import "fmt"

func main() {
    var a [5]int
    fmt.Println("empty:", a)

    a[4] = 100
    fmt.Println("set:", a)
    fmt.Println("get:", a[4])

    fmt.Println("length:", len(a))

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println("init:", b)
}

输出

empty: [0 0 0 0 0]
set: [0 0 0 0 100]
get: 100
length: 5
init: [1 2 3 4 5]

解释:数组长度固定,类型必须相同。可以在声明时初始化数组。

切片

详解

切片在功能上比数组更灵活,但是背后的数组大小是固定的。切片则可以在 append 运算中动态增长和收缩。

技巧

  • 使用 make 来创建一个初始值为零的切片。
  • 切片的 cap 可以获取切片的最大容量。

样例代码

package main

import "fmt"

func main() {
    s := make([]string, 3)
    fmt.Println("emp:", s)

    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("set:", s)
    fmt.Println("get:", s[2])

    fmt.Println("len:", len(s))

    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println("apd:", s)

    c := make([]string, len(s))
    copy(c, s)
    fmt.Println("cpy:", c)
}

输出

emp: [  ]
set: [a b c]
get: c
len: 3
apd: [a b c d e f]
cpy: [a b c d e f]

解释:切片不需要声明长度,允许动态增加元素。可以使用 make 创建空切片,使用 append 向切片添加元素。还可以从现有切片中获取子切片。

Map

详解

Map 是 Go 的内建关联数据类型。其他编程语言可能称此为哈希表或字典。

技巧

  • 使用 delete 可以从 map 中删除键值对。
  • 通过键值获取时,可选的第二返回值表示键是否在 map 中。

样例代码

package main

import "fmt"

func main() {
    m := make(map[string]int)

    m["k1"] = 7
    m["k2"] = 13
    fmt.Println("map:", m)

    v1 := m["k1"]
    fmt.Println("v1:", v1)

    fmt.Println("length:", len(m))

    delete(m, "k2")
    fmt.Println("delete:", m)

    _, present := m["k2"]
    fmt.Println("present:", present)
}

输出

map: map[k1:7 k2:13]
v1: 7
length: 2
delete: map[k1:7]
present: false

解释:可以使用 make 创建空映射。可以使用 delete 从映射中删除键值对。

Range

详解

range 在 Go 中用于对切片、数组、字符串或 map 进行迭代。

技巧

  • 如果你只想获取元素的索引,忽略实际值,则可以省略“值”变量。

样例代码

package main

import "fmt"

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

    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }

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

    for k := range kvs {
        fmt.Println("key:", k)
    }

    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

输出

sum: 9
index: 1
a -> apple
b -> banana
key: a
key: b
0 103
1 111

解释:range 可以用于迭代数组、切片、映射、字符串的元素。

函数

详解

函数是 Go 的核心。你可以定义函数接受零个或多个参数。

技巧

  • Go 支持多返回值,这一点在错误处理等方面非常实用。

样例代码

package main

import "fmt"

func plus(a int, b int) int {
    return a + b
}

func plusPlus(a, b, c int) int {
    return a + b + c
}

func main() {
    res := plus(1, 2)
    fmt.Println("1+2 =", res)

    res = plusPlus(1, 2, 3)
    fmt.Println("1+2+3 =", res)
}

输出

1+2 = 3
1+2+3 = 6

解释:定义函数使用 func 关键字。函数可以有参数和返回值。参数和返回值的类型必须在函数签名中声明。

指针

详解

Go 支持指针,允许传递对象的内存地址而不是对象本身。

技巧

  • 指针对于性能关键的代码或大型结构体尤为有用。
  • Go 不支持指针算术。

样例代码

package main

import "fmt"

func zeroval(ival int) {
    ival = 0
}

func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("initial:", i)

    zeroval(i)
    fmt.Println("zeroval:", i)

    zeroptr(&i)
    fmt.Println("zeroptr:", i)

    fmt.Println("pointer:", &i)
}

输出

initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x4e13110a

解释:Go 支持指针,允许通过引用直接访问和修改内存地址的值。通过使用 * 操作符来读取指针指向的内存地址的值,或者使用 & 操作符来获取变量的内存地址。

结构体

详解

Go 的结构体是将字段聚集在一起的数据结构。

技巧

  • 使用点号来访问结构体字段。
  • 结构体也可以是字段的类型。

样例代码

package main

import "fmt"

type person struct {
    name string
    age  int
}

func main() {
    fmt.Println(person{"Bob", 20})

    fmt.Println(person{name: "Alice", age: 30})

    fmt.Println(person{name: "Fred"})

    fmt.Println(&person{name: "Ann", age: 40})

    s := person{name: "Sean", age: 50}
    fmt.Println(s.name)

    sp := &s
    fmt.Println(sp.age)

    sp.age = 51
    fmt.Println(sp.age)
}

输出

{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
Sean
50
51

解释:结构体是用于封装多个字段的复合数据类型。可以通过点号访问结构体的字段。

接口

详解

接口是方法签名的命名集合。

技巧

  • 在实现接口的类型上调用接口方法时,接收器参数自动转换为接口类型。

样例代码

package main

import (
    "fmt"
    "math"
)

type geometry interface {
    area() float64
    perim() float64
}

type rect struct {
    width, height float64
}

type circle struct {
    radius float64
}

func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    measure(c)
}

输出

{3 4}
12
14
{5}
78.53981633974483
31.41592653589793

解释:接口是方法特征的命名集合。如果某个类型实现了接口中定义的所有方法,则此类型实现了该接口。

错误处理

详解

Go 通过返回错误值来进行错误处理。与使用异常的语言相比,这种错误处理方法更加明确、可预测。

技巧

  • 使用自定义错误类型提供关于错误的更多上下文。
  • 使用 errors.New 创建简单的错误消息。

样例代码

package main

import (
    "errors"
    "fmt"
)

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
}

输出

f1 worked: 10
f1 failed: can't work with 42

解释:Go 通常通过返回一个错误值来进行错误处理。这与许多其他语言使用异常的方法不同。

协程

详解

协程(Goroutines)是与其他函数并行运行的函数。它们是轻量级的线程。

技巧

  • go f(x, y, z) 开启一个新的 Goroutine。

样例代码

package main

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {
    f("direct")

    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")

    time.Sleep(time.Second)
    fmt.Println("done")
}

输出

direct : 0
direct : 1
direct : 2
goroutine : 0
goroutine : 1
goroutine : 2
going
done

解释:协程是轻量级的线程,可以并发执行。通过在调用函数之前使用 go 关键字来启动协程。

通道

详解

通道是连接多个 Go 协程的管道。你可以从一个 Go 协程将值发送到通道,然后在其他 Go 协程中接收。

技巧

  • 使用 make(chan val-type) 创建新的通道。
  • 通道操作符 <- 用于发送和接收值。

样例代码

package main

import "fmt"

func main() {
    messages := make(chan string)

    go func() { messages <- "ping" }()

    msg := <-messages
    fmt.Println(msg)
}

输出

ping

解释:通道允许在两个或更多的协程之间安全地通信。你可以将值发送到通道,并从通道接收值。

Panic

详解

panic 意味着有些出乎意料的错误发生。通常我们用它来表示程序正常操作中不应该出现的或者我们没有准备好正确处理的错误。

技巧

  • 避免在普通操作中使用 panic
  • panic 对于程序员错误和不可恢复的情况是有用的。

样例代码

package main

import "os"

func main() {
    panic("a problem")

    _, err := os.Create("/tmp/file")
    if err != nil {
        panic(err)
    }
}

解释:当程序中出现无法继续运行的严重错误时,可以使用 panic 使程序中止。

Defer

详解

defer 用于确保函数调用在程序执行结束后执行。通常用于资源清理。

技巧

  • defer 常用于关闭文件、解锁资源、数据库连接关闭等。

样例代码

package main

import "fmt"

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}

输出

hello
world

解释:defer 语句延迟调用一个函数直到包含 defer 语句的函数执行完毕。

Recover

详解

recover 是一个内建的函数,可以用来捕获和处理panic异常。

技巧

  • 在延迟函数中使用recover来捕获和处理异常。

样例代码

package main

import "fmt"

func main() {
    f := func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in f", r)
            }
        }()
        fmt.Println("Calling g.")
        g(0)
        fmt.Println("Returned normally from g.")
    }

    f()
    fmt.Println("Returned normally from f.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

输出

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

解释:recover 函数用于从 panic 中恢复,重新获得协程的控制权。