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 语言支持三种基本的控制流程语句:if、for 和 switch。
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:这种形式是一个无限循环,只能通过 break 或 return 来跳出循环。例如,模拟一个掷骰子的过程:
for {
n := rand.Intn(6) + 1 // 随机生成一个1到6的整数
fmt.Println(n)
if n == 6 {
break // 如果是6,就结束循环
}
}
switch
switch 语句用于根据一个表达式的值来执行不同的分支。switch 语句的格式如下:
switch 表达式 {
case 值1:
// 分支1
case 值2:
// 分支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的元素
切片的长度和容量可以通过 len 和 cap 函数来获取。切片的长度是指切片中元素的个数,切片的容量是指切片在底层数组中能够扩展的最大长度。例如:
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 函数执行完毕,并获取结果后再继续执行。