Go 语言入门指南:基础语法和常用特性解析 | 青训营

65 阅读10分钟

Go 语言是一门简洁、高效、并发的编程语言,适合开发各种类型的后端应用。本文将介绍 Go 语言的基础语法和常用特性,包括:

  • 变量、常量和类型
  • 函数和方法
  • 控制流程
  • 数组、切片和映射
  • 结构体和接口
  • 并发编程

变量、常量和类型

Go 语言是一门静态类型的语言,也就是说每个变量都有一个确定的类型,在编译时就已经确定。Go 语言支持基本的数据类型,如字符串、整数、浮点数、布尔值等,以及复合的数据类型,如数组、切片、映射、结构体、接口等。

变量

变量是程序中可以改变的值,可以使用 var 关键字来声明一个变量,并给它一个初始值。例如:

var name string = "Alice"
var age int = 18

如果变量的初始值已经确定了,可以省略类型,让编译器自动推断。例如:

var name = "Alice"
var age = 18

也可以使用短变量声明 := 来同时声明和初始化一个变量,这种方式只能在函数内部使用。例如:

name := "Alice"
age := 18

常量

常量是程序中不会改变的值,可以使用 const 关键字来声明一个常量,并给它一个初始值。例如:

const pi = 3.14
const hello = "Hello, world!"

函数和方法

函数

函数是一段可以被重复调用的代码块,可以接受零个或多个参数,并返回零个或多个结果。函数的定义格式如下:

func 函数名(参数列表) (返回值列表) {
    // 函数体
}

例如,定义一个计算两个数之和的函数:

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

调用函数时,需要按照函数定义的顺序传递参数,并接收返回值。例如:

x := add(1, 2) // x = 3

如果函数有多个返回值,可以使用多个变量来接收,或者使用 _ 来忽略不需要的返回值。例如,定义一个交换两个数的函数:

func swap(a int, b int) (int, int) {
    return b, a
}

调用函数时,可以这样写:

x, y := swap(1, 2) // x = 2, y = 1
_, z := swap(3, 4) // z = 3, _ = 4

方法

方法是一种特殊的函数,它绑定在某个类型上,可以通过该类型的实例来调用。方法的定义格式如下:

func (接收者类型 接收者名称) 方法名(参数列表) (返回值列表) {
    // 方法体
}

例如,定义一个表示人的结构体,并为其定义一个打招呼的方法:

type Person struct {
    name string
    age int
}

func (p Person) sayHello() {
    fmt.Println("Hello, I am", p.name)
}

调用方法时,需要先创建一个结构体实例,并通过 . 操作符来调用方法。例如:

p := Person{name: "Alice", age: 18}
p.sayHello() // Hello, I am Alice

控制流程

控制流程是指程序执行的顺序和分支。Go 语言支持三种基本的控制流程语句:ifforswitch

if

if 语句用于根据一个布尔表达式的值来执行不同的分支。if 语句的格式如下:

if 布尔表达式 {
    // 分支1
} else if 布尔表达式 {
    // 分支2
} else {
    // 分支3
}

例如,判断一个数是否为偶数:

n := 10
if n % 2 == 0 {
    fmt.Println(n, "is even")
} else {
    fmt.Println(n, "is odd")
}

for

for 语句用于重复执行一段代码,直到满足某个条件。for 语句有三种形式:

  • for 初始化; 条件; 后续:这种形式类似于其他语言的 for 循环,每次循环前会检查条件是否为真,每次循环后会执行后续操作。例如,打印 1 到 10 的数字:
for i := 1; i <= 10; i++ {
    fmt.Println(i)
}
  • for 条件:这种形式类似于其他语言的 while 循环,只要条件为真就会一直循环。例如,计算斐波那契数列的前 10 项:
a, b := 0, 1
for a < 100 {
    fmt.Println(a)
    a, b = b, a + b
}
  • for:这种形式是一个无限循环,只能通过 breakreturn 来跳出循环。例如,模拟一个掷骰子的过程:
for {
    n := rand.Intn(6) + 1 // 随机生成一个1到6的整数
    fmt.Println(n)
    if n == 6 {
        break // 如果是6,就结束循环
    }
}

switch

switch 语句用于根据一个表达式的值来执行不同的分支。switch 语句的格式如下:

switch 表达式 {
case1:
    // 分支1
case2:
    // 分支2
default:
    // 默认分支
}

例如,判断一个字母的大小写:

c := 'A'
switch c {
case 'A', 'E', 'I', 'O', 'U':
    fmt.Println(c, "is a capital vowel")
case 'a', 'e', 'i', 'o', 'u':
    fmt.Println(c, "is a lowercase vowel")
default:
    fmt.Println(c, "is not a vowel")
}

数组、切片和映射

数组

数组是一种固定长度的序列,可以存储相同类型的元素。数组的定义格式如下:

var 数组名 [长度]类型

例如,定义一个长度为 5 的整数数组,并给它赋值:

var arr [5]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5

也可以使用字面量来初始化一个数组,省略长度或使用 ... 让编译器自动推断。例如:

arr := [5]int{1, 2, 3, 4, 5} // 显式指定长度为5
arr := [...]int{1, 2, 3, 4, 5} // 使用...让编译器推断长度为5
arr := []int{1, 2, 3, 4, 5} // 省略长度,实际上创建了一个切片,而不是数组

切片

切片是一种可变长度的序列,可以存储相同类型的元素。切片是对数组的一个视图,可以通过指定数组的起始和结束索引来创建一个切片。切片的定义格式如下:

var 切片名 []类型

例如,定义一个整数切片,并给它赋值:

var s []int
s = []int{1, 2, 3, 4, 5} // 使用字面量赋值
s = arr[1:4] // 使用数组的切片赋值,包含索引1到3的元素

切片的长度和容量可以通过 lencap 函数来获取。切片的长度是指切片中元素的个数,切片的容量是指切片在底层数组中能够扩展的最大长度。例如:

fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 4

切片可以通过 append 函数来添加新的元素,如果切片的容量不足,会自动分配一个更大的底层数组。例如:

s = append(s, 6) // 添加一个元素6
fmt.Println(len(s)) // 4
fmt.Println(cap(s)) // 8

切片可以通过 copy 函数来复制另一个切片的内容,复制的长度取决于两个切片中较短的一个例如:

t := []int{7, 8, 9}
copy(s, t) // 复制t到s
fmt.Println(s) // [7, 8, 9, 6]

映射

映射是一种键值对的集合,可以存储不同类型的键和值。映射的定义格式如下:

var 映射名 map[键类型]值类型

例如,定义一个映射,用于存储人名和年龄:

var m map[string]int
m = make(map[string]int) // 使用make函数创建一个空映射
m["Alice"] = 18 // 添加一个键值对
m["Bob"] = 20 // 添加另一个键值对

也可以使用字面量来初始化一个映射,省略类型或使用 ... 让编译器自动推断。例如:

m := map[string]int{"Alice": 18, "Bob": 20} // 显式指定类型为map[string]int
m := map[string]int{"Alice": 18, "Bob": ...} // 使用...让编译器推断类型为map[string]int
m := {"Alice": 18, "Bob": 20} // 省略类型,实际上创建了一个结构体,而不是映射

映射的长度可以通过 len 函数来获取,表示映射中键值对的个数。例如:

fmt.Println(len(m)) // 2

映射可以通过 delete 函数来删除某个键及其对应的值。例如:

delete(m, "Alice") // 删除键为"Alice"的键值对
fmt.Println(len(m)) // 1

映射可以通过索引操作符来访问或修改某个键对应的值。如果键不存在,会返回值类型的零值。也可以通过第二个返回值来判断键是否存在。例如:

age, ok := m["Alice"] // 访问键为"Alice"的值和是否存在标志
if ok {
    fmt.Println(age) // 如果存在,打印年龄
} else {
    fmt.Println("Not found") // 如果不存在,打印"Not found"
}
m["Alice"] = 19 // 修改键为"Alice"的值为19

结构体和接口

结构体

结构体是一种自定义的数据类型,可以将不同类型的字段组合在一起。结构体的定义格式如下:

type 结构体名 struct {
    字段名 类型
    字段名 类型
    ...
}

例如,定义一个表示人的结构体,包含姓名和年龄两个字段:

type Person struct {
    name string
    age int
}

创建一个结构体实例,可以使用 new 函数或字面量。例如:

p := new(Person) // 使用new函数创建一个空结构体
p.name = "Alice"
p.age = 18

q := Person{name: "Bob", age: 20} // 使用字面量创建一个结构体,并初始化字段

访问或修改结构体的字段,可以使用 . 操作符。例如:

fmt.Println(p.name) // Alice
fmt.Println(q.age) // 20
q.age++ // 增加年龄

接口

接口是一种抽象的数据类型,可以定义一组方法的签名,但不需要实现。接口的定义格式如下:

type 接口名 interface {
    方法名(参数列表) (返回值列表)
    方法名(参数列表) (返回值列表)
    ...
}

例如,定义一个表示动物的接口,包含两个方法:叫和跑:

type Animal interface {
    sound() string // 返回动物的叫声
    run() int // 返回动物的速度
}

实现一个接口,只需要定义一个类型,并为该类型实现接口中的所有方法。例如,定义一个表示狗的结构体,并实现动物接口:

type Dog struct {
    name string
    speed int
}

func (d Dog) sound() string {
    return "Woof"
}

func (d Dog) run() int {
    return d.speed
}

使用一个接口,可以通过多态的方式来处理不同类型的实例。例如,定义一个函数,接受一个动物接口作为参数,并打印它的信息:

func printAnimal(a Animal) {
    fmt.Println("This animal sounds like", a.sound())
    fmt.Println("This animal runs at", a.run(), "km/h")
}

调用这个函数时,可以传入任何实现了动物接口的类型。例如:

d := Dog{name: "Spot", speed: 30}
printAnimal(d) // This animal sounds like Woof. This animal runs at 30 km/h.

并发编程

并发编程是指让程序可以同时执行多个任务,提高效率和性能。Go 语言支持两种并发编程的方式:goroutine 和 channel。

goroutine

goroutine 是一种轻量级的线程,可以在一个程序中创建成千上万个。goroutine 的定义格式如下:

go 函数名(参数列表)

例如,定义一个打印数字的函数,并在一个 goroutine 中调用它:

func printNum(n int) {
    for i := 0; i < n; i++ {
        fmt.Println(i)
    }
}

go printNum(10) // 在一个新的goroutine中执行printNum函数

channel

goroutine 之间可以通过 channel 来通信和同步。channel 是一种类似于管道的数据结构,可以让一个 goroutine 向另一个 goroutine 发送或接收数据。channel 的定义格式如下:

var channel名 chan 类型

例如,定义一个整数类型的 channel,并使用 make 函数来创建它:

var ch chan int
ch = make(chan int)

向 channel 中发送数据,可以使用 <- 操作符。例如,在一个 goroutine 中向 channel 中发送一个数字:

go func() {
    ch <- 42 // 向ch中发送42
}()

从 channel 中接收数据,也可以使用 <- 操作符。例如,在另一个 goroutine 中从 channel 中接收一个数字,并打印它:

go func() {
    n := <- ch // 从ch中接收一个数字,并赋值给n
    fmt.Println(n) // 打印n
}()

channel 的发送和接收操作是阻塞的,也就是说,如果没有数据可发送或接收,goroutine 会等待直到有数据可用。这样可以实现 goroutine 之间的同步。例如,定义一个计算两个数之和的函数,并在一个 goroutine 中调用它,并将结果发送到 channel 中:

func add(a int, b int, ch chan int) {
    c := a + b // 计算两个数之和
    ch <- c // 将结果发送到ch中
}

go add(1, 2, ch) // 在一个新的goroutine中执行add函数,并传入ch作为参数

在主 goroutine 中,从 channel 中接收数据,并打印它:

result := <- ch // 从ch中接收数据,并赋值给result
fmt.Println(result) // 打印result

这样可以保证主 goroutine 会等待 add 函数执行完毕,并获取结果后再继续执行。