前言
Go语言以其简洁的语法,"少即是多"的设计哲学,和原生支持高并发的特点,已经成为云原生和后端高并发应用的热门选择。
本人作为一名从Java转向Go的开发者,在初学Go经常会困扰于语法,遂整理该文帮助广大开发者快速入门Go语言。
基础入门
关键字一览
| 关键字 | 作用描述 |
|---|---|
break |
跳出当前循环(for)、switch 或 select 结构,可配合标签跳出多层结构。 |
case |
用于 switch 或 select 中,定义分支条件,匹配对应的值或通道操作。 |
chan |
声明通道(channel)类型 |
const |
声明常量,支持批量声明。 |
continue |
跳过本次迭代的剩余代码,直接进入下一次迭代,支持标签跳转。 |
default |
用于 switch 或 select 中,定义默认分支(当其他所有 case 不匹配时执行)。 |
defer |
声明延迟执行的函数,在当前函数返回前(无论正常返回还是 panic)执行,常用于资源清理。 |
else |
与 if 配合使用,定义条件不满足时的分支代码块。 |
fallthrough |
仅用于 switch 中,强制执行下一个 case。 |
for |
唯一的循环语句 |
func |
声明函数或方法,定义可执行的代码块,支持多返回值、匿名函数等。 |
go |
用于启动 goroutine,将函数调用放入后台并发执行。 |
goto |
跳转到函数内的标签位置,需谨慎使用 |
if |
定义条件判断语句 |
import |
导入其他包 |
interface |
声明接口类型 |
map |
声明映射(map)类型 |
package |
声明当前文件所属的包 |
range |
配合 for 使用,用于遍历切片、映射、字符串、数组、通道等,返回索引 / 键和对应的值。 |
recover |
仅在 defer 中使用,用于捕获 panic 触发的异常,阻止程序崩溃并返回错误值。 |
return |
用于函数中,终止当前函数执行并返回指定结果(无返回值时可省略)。 |
select |
用于监听多个通道的操作(发送或接收),当其中一个通道可操作时执行对应分支。 |
struct |
声明结构体类型 |
switch |
多分支条件匹配语句 |
type |
定义类型别名(如 type MyInt int)或自定义类型(如结构体、接口等)。 |
var |
声明变量,未初始化变量会被赋予零值。 |
基本数据类型
| 类型类别 | 具体类型 | 描述 |
|---|---|---|
| 布尔类型 | bool |
表示逻辑真假 |
| 整数类型(有符号) | int |
与系统位数一致 |
int8 |
8 位有符号整数 | |
int16 |
16 位有符号整数 | |
int32/rune |
32 位有符号整数,rune用于表示 Unicode 码点(支持中文、 emoji 等) | |
int64 |
64 位有符号整数 | |
| 整数类型(无符号) | uint |
与系统位数一致 |
uint8/byte |
8 位无符号整数,byte用于表示 ASCII 字符 | |
uint16 |
16 位无符号整数 | |
uint32 |
32 位无符号整数 | |
uint64 |
64 位无符号整数 | |
uintptr |
用于存储指针地址的无符号整数(位数与指针一致) | |
| 浮点数类型 | float32 |
32 位单精度浮点数 |
float64 |
64 位双精度浮点数 | |
| 字符串类型 | string |
表示字符串 |
| 复数类型 | complex64 |
实部(float32) + 虚部(float32) |
complex128 |
实部(float64) + 虚部(float64) |
复合数据类型
| 类型类别 | 结构 | 描述 |
|---|---|---|
| 数组 | [<数组长度 >]<元素类型> | 如:[5]int 为存储5个int类型元素的数组 |
| 切片 | []<元素类型> | 如:[]int 为存储int类型元素的切片,长度由编译器动态扩充 |
| 映射 | map[<key类型 >]<value类型> | 如:map[string]int 为存储以string为键,以int为值的映射集合 |
| 结构体 | 自定义结构字段 | 只定义属性字段,不在声明中囊括方法 |
| 函数 | 自定义函数签名 | 将指定签名的函数作为一种类型 |
| 接口 | 自定义函数签名的集合 | 定义一组函数但不实现 |
| 通道 | chan <元素类型> | 如:chan int 为传输int类型元素的通道 |
函数作为类型是Go中的特色,各类型会在后文中详细讲解。
变量的声明与赋值
基本类型
在Go语言中,变量名在前,变量类型在后。
声明的变量必须使用至少1次,否则编译报错。
1. 先声明后赋值
- 单变量
go
var a int
a = 5
- 多变量
go
var a, b, c int //均为int类型
a, b, c = 10, 20, 30
2. 声明+赋值
- 显式指定类型
go
var a int = 5
const PI float64 = 3.14159
- 隐式推导类型
go
var height = 1.73 // go编译器会根据等号右边的值推断左边的变量类型
var isOk = true
const PI = 3.14159
- 短变量声明
Go中提供了特殊的语法糖 ":=" 以简化声明与赋值,但只能在函数内部使用
go
a := 5 // 声明变量a,类型由编译器推导,并赋值为5
- 批量声明与赋值
go
var (
id int = 23 //显式指定类型
name = "zhangsan" // 隐式类型推导
weight = 57.68
)
复合类型
本部分只涉及数组、切片、映射、通道的初步构建与使用,更多细节及结构体、函数和接口会写在后续对应章节中。
1. 数组
数组为值类型,存储的是值本身
先声明后赋值
go
var array [3]int // 初始化array为{0, 0, 0}
array = [3]int{1, 2} // 将array赋值为{1, 2, 0}
声明+赋值
go
// 显式指定类型
var array1 [3]int = [3]int{1, 2, 3}
// 隐式类型推导
var array2 = [3]int{1, 2}
// 短变量声明(语法糖)
array3 := [3]int{1, 2, 3}
2. 切片
切片(slice)是对数组的上层抽象,能够根据元素的格式进行动态扩容,属于引用类型,变量名存储内存地址。这里引入两个名词:长度、容量。
长度表示底层数组实际存储的元素个数。
容量表示初始化时切片时为底层数组分配的初始空间。当长度大于容量时,Go编译器会重新分配更大容量的数组,通常是双倍扩容,并将原数组中的元素拷贝至新的扩容数组。

先声明后赋值
go
var slice1 []int // 初始化slice1为nil(零值)
slice1 = []int{2, 3, 4}
slice1[2] = 10 // 此时slice1变为了{2, 3, 10}
var slice2 []float64
slice = []float64{3.28}
声明+赋值
go
// 显式指定类型
var slice3 []int = []int{1, 2, 3}
// 隐式类型推导
var slice4 = []int{1, 2, 3}
// 语法糖:=
slice5 := []int{4, 5, 6}
使用make函数初始化
make是Go中内置的函数,用于初始化切片、映射和通道类型的变量,对于切片来说具体的参数定义为make([]T, len, cap),其中cap参数为可选参数,默认与len相等。
这种初始化方式也是官方所推荐的。
go
slice6 := make([]bool, 2, 5) // slice6被初始化为{false, false, _, _, _}
slice6[1] = true // 此时变为{false, true, _, _, _}
slice6 = []bool{true, true} // 一次性赋多个值也是ok的
3. 映射
map同样具有容量的概念,但是不需要初始长度,其长度会随着向map添加KV对而动态变化。
对于map而言,零值KV对的初始化是无意义的,也正因如此,在对未初始化的map进行赋值会触发panic,panic 是一种运行时错误处理机制,用于表示程序遇到了无法正常恢复的严重错误。
先声明后赋值
go
var m map[string]string
m = map[string]string{"张三": "10220308"} // 将m初始化为{"张三": "10220308"}
如下示例会触发panic:
go
var m map[string]string // 只声明,未初始化
m["张三"] = "10220308" // 尝试为m赋值, 触发panic
声明+赋值
go
// 显式指定类型
var m1 map[string]string = map[string]string{"zhangsan": "10220308"}
// 隐式类型推导
var m2 = map[string]string{"zhangsan": "10220308", "lisi": "10220309"}
使用make函数初始化
对于map类型,make的签名为:make(map[key类型]value类型, 初始容量),初始容量是一个可选参数,若不传该参数,编译器会使用默认容量进行初始化。
默认容量随着Go版本的更新会发生变化,作为使用者,我们无需关心其具体值。
go
m3 := make(map[string]string, 5) // 初始化容量为5的map容器
m3["zhangsan"] = "10220308" // 将"zhangsan": "10220308"添加到容器中
4. 通道
通道(channel)是Go中支持高并发的核心类型,以chan关键字表示。
依据数据的流向分为三类:
1.双向通道
语法结构为: chan 元素类型
2.只读通道
语法结构为: <-chan 元素类型
3.只写通道
语法结构为: chan<- 元素类型
需要注意的是,权限扩大是被禁止的,例如:将一个只读通道赋值给双向通道
使用make函数初始化
注意,通道类型只支持使用make函数初始化。
make函数的签名为:make(chan 元素类型, [缓冲区大小])
go
// 初始化一个元素类型为int,缓冲区大小为5的双向通道并赋值给变量ch1
ch1 := make(chan int, 3)
向通道写入值
go
// 写入的值会被存入缓冲区中
ch1 <- 10
ch1 <- 20
从通道读取值
go
// 从缓冲区中读取值并赋值给变量a, b
a := <-ch1
b := <-ch1
流程控制
值得一提的是,在Go中,break和continue关键字均支持标签跳转,意味着可以一次性跳出多层循环,但这种做法与goto关键字一样,会破坏代码的结构性,存在着争议。
if ... else条件判断
- 条件无需括号包裹,代码块必须以{ }包裹,即使只有一条语句
- if条件前可执行变量的初始化,作用域仅限
if...else...块
go
if flag := true; flag {
fmt.Println("if 块已执行") // 在控制台打印 "if 块已执行"
}else {
fmt.Println("显然这句话不会被打印")
}
for 循环
在Go语言中, 关键字for是唯一能定义循环的,没有其他语言中的while,do-while等,但却可以实现其功能。
- 标准for循环
go
for i:=0; i<10; i++ {
// 你的代码
}
- while循环
go
flag := true
// flag条件可省略,省略后为死循环,需配合break使用
for flag {
// 你的代码
}
- for range遍历
用于迭代切片、映射、通道等类型的变量,以切片为例
go
slice := []int{1, 2, 3, 4, 5}
// 当传入参数为切片类型时,range函数会返回每次迭代的值和索引供后续逻辑使用
// 若无需使用索引,可用"_"作为占位符
// 使用索引和值:
for index, value := range(slice) {
// 你的代码
}
// 使用值:
for _, value := range(slice) {
// 你的代码
}
// 使用索引:
for index := range(slice) {
// 你的代码
}
switch匹配
- 表达式支持任意类型,甚至可以省略
go
// 条件前初始化,分号后省略条件,相当于多个if...else...
switch score := 89; {
case score >= 90:
fmt.Println("优秀")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
- 自动break,每个case执行完自动跳出。若需执行下一个case,需使用
fallthrough强制执行。
go
switch x := 2; x {
case 1:
fmt.Println(1)
case 2:
fmt.Println(2)
fallthrough // 强制执行下一个case
case 3:
fmt.Println(3) // 会被执行
}
- 支持多值匹配,使用","分隔
go
switch day := "wed"; day {
case "mon", "tue", "wed", "thu", "fri":
fmt.Println("工作日")
case "sat", "sun":
fmt.Println("周末")
}
goto跳转
- 标签必须在当前函数内
go
// 错误示例
func f1() {
label: // 标签在f1中
fmt.Println("f1")
}
func main() {
goto label // 错误:label不在main函数内
}
- 不能跳过变量声明
go
// 错误示例
func main() {
goto skip // 尝试跳转到变量x声明之后
// 变量声明(被跳过)
x := 10
skip:
fmt.Println(x) // 错误:x在跳转后未声明就被使用
}
- 标签不能重复定义
函数
在Go语言中,函数的作用域由函数名称的首字母决定。
大写字母表示该函数包外可访问,小写字母则说明该函数只能在包内访问。
此外,多返回值 、函数作为类型 也是Go的语法特色。
定义
语法结构:func 函数名称 (参数列表 )(返回值列表 ),通常我们也把函数的参数类型列表 + 返回值类型列表 统称为函数的签名。
go
// b, c两个变量简写了类型,均为int
// 首字母大写,允许包外访问
func FirstFunction(a string, b, c int) (int, float64){
// 你的代码
// 返回值需和签名中定义的类型、数量保持一致
return 3, 6.48
}
返回值
单返回值
若某个函数只有一个返回值,则括号可以省略
go
// 省略返回值列表括号
// 首字母小写,仅限包内访问
func secondFunction(d bool) bool{
// 你的代码
return true
}
多返回值
Go语言中,多返回值函数是非常常见的。
此外,还可以给返回值命名并直接在函数中使用,在return时省略返回对象。
举个栗子,执行完某个函数后将计算结果和错误信息一并返回。
go
func returnWithMultipleResults(a, b float64) (result float64, msg string){
// 给返回值赋予默认值,未赋予默认值的返回值会默认返回对应类型的零值
result = 0.0
if b == 0.0 {
msg = "除数不能为0"
return
}
result = a / b
// 省略返回对象result和msg
return
}
特殊函数
匿名函数
对于只使用1次而无需复用的函数,可以省略函数名称并直接调用。
请注意,匿名函数可以在函数内定义。
go
// 定义一个无参无返回值的匿名函数并调用
func main(){
func (){
fmt.Println("这是一个匿名函数") // 在控制台打印 "这是一个匿名函数"
}() // 使用()进行函数的调用
}
高阶函数
Go同样支持函数式编程,这意味着函数可以作为参数被传递、作为返回值被返回,给予开发者很高的灵活性。
go
// f1接收一个签名为func(a, b int) int类型的函数,命名为f
// 在内部将20,10作为参数传递给f,并调用
// 将f返回的结果赋值给result并返回
func f1(f func(a, b int) int) int {
c, d := 20, 10
result := f(c, d)
return result
}
func add(a, b int) int{
return a + b
}
func sub(a, b int) int{
return a - b
}
func main(){
// f1可以根据传入的函数不同而执行不同的逻辑
result1 := f1(add) // result1=30
result2 := f1(sub)// result2=10
result3 := f1(func(c, d int)int{
return c * d
})// result3 = 200
}
延迟函数
延迟函数也是Go中的一大特色,常用于简化资源管理和确保清理操作。
语法结构为:defer 函数调用
在C/C++语言中,经常需要在函数的末尾释放申请的内存;在Java语言中,也需要在合适的地方手动释放锁。
而在Go语言中,可以在相关操作的下一行直接释放资源(例如释放锁),只需在函数调用前加上关键字defer。
延迟函数会在其所在方法、函数执行完毕后才执行 ,并且一定会执行 ,即使该方法、函数报错或出现panic。
若存在多个延迟函数,则会依照后进先出的顺序执行
值得注意的是,延迟函数的参数会在defer声明时立即求值,而非在执行时求值
go
func main() {
// 调用os包下的公开函数Open,并将返回值依次赋值给file, err
// file, err 是自定义的结构体类型,这里无需过多关心
file, err := os.Open("test.txt")
// Close函数会在main函数执行完后执行
defer file.Close()
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
// 读取文件内容(示例操作)
fmt.Println("文件已打开,准备读取...")
}
函数作为类型
不同于其他语言,在Go语言中,函数是一等公民,可以像int、float64等基本类型一样被赋值和传递。
定义
语法结构为:type 类型名称 函数签名
go
// 将接受两个int类型的参数,返回值为int类型的函数定义为MathFunc类型
type MathFunc func(int, int) int
声明与赋值
go
// 同类型参数类型简写
func Add(a, b int) int{
return a + b
}
func Sub(a, b int) int{
return a - b
}
func main(){
var f MathFunc // 声明一个类型为MathFunc的变量f
f = Add // 将f赋值为Add函数
result1 := f(2, 3) // 调用f,并将返回值5赋值给result1变量,result1=5
f = Sub
result2 := f(12, 5)// result=7
}
结构体与方法
基于面向对象的思想,Go也提供了类似C/C++语言的实现方式,即结构体类型。
与Java所不一样的是,Go中的结构体只定义属性,而非像Java一样,将属性和行为(方法)全部封装在一起。
定义
go
// 只定义属性
type Person struct{
name string
age int
height float64
weight float64
hobby []string
}
声明与赋值
值类型
go
var person Person // 值类型,在声明之初,已经完成内存的分配,并将各字段赋值为对应类型的零值
person.height = 1.73 // 此处将height赋值为1.73
// 根据字面量为对应的字段赋值
// 由于未指定height的值,又将height赋值为float64类型的零值了
person = Person{
name: "张三",
age: 18,
hobby: []string{"羽毛球", "乒乓球"}, // 末尾行的","不能省
}
//
指针类型
go
var person2 *Person // 声明为指针,初始值是nil
person2 = &Person{ // 取完成初始化的内存地址,赋值给变量person2
name: "李四",
age: 20,
}
注意,在未初始化前赋值会在运行时报错,本质上该操作在尝试为nil赋值,如下示例:
go
var person2 *Person // 声明为指针,初始值是nil
person2.name = "李四" // 这里报错
使用new函数获取指针实例
new是一个内置函数,会返回初始化后的结构体指针
语法结构:new(结构体名)
go
// person3的类型为 *Person,所有字段初始化为零值
person3 := new(Person)
组合
Go中没有继承的概念,通过结构体的嵌套(组合)实现代码的复用
命名嵌套
go
type Address struct{
Province string
City string
}
// Person在外层,Address在内层
type Person struct{
name string
age int
height float64
weight float64
hobby []string
Addr Address // 嵌套Address结构体并命名为Addr
}
func main(){
p := new(Person)
name := p.name // 访问外层字段
city := p.Addr.City // 访问内层字段
}
匿名嵌套
- 字段名默认与类型名相同
- 内层字段和方法会被"提升"到外层,可直接访问
go
type Address struct{
Province string
City string
}
// Person在外层,Address在内层
type Person struct{
name string
age int
height float64
weight float64
hobby []string
Address // 嵌套Address结构体,字段名默认为Address
}
func main(){
p := new(Person)
name := p.name // 访问外层字段
city := p.City // 访问内层字段
}
接收者
有了属性后,我们已经能够通过Person类型去描述一个人,但我们只知道他的信息,却无法定义他的行为,即能够做什么。
Go语言通过接收者将类型与函数相关联,关联到特定类型的函数称为方法。
对于结构体而言,方法可以操作结构体的字段,定义其行为。
定义
语法结构:func (接收者名称 接收者类型 ) 方法名 (参数列表 )(返回值列表)
go
func (p Person) Eat(){
fmt.Println("干饭中。。。")
}
值接收者
值接受者传入的是调用实例的副本,适用于只读取信息的场景
go
func main(){
person := new(Person)
person.Eat() // 使用person实例调用Eat方法,方法内可通过接收者名称p访问实例副本
}
指针接收者
传入实例的指针,可修改原实例
go
func (per *Person) SetName(name string){
per.name = name
}
func main(){
person := new(Person)
name := "孙六"
person.SetName(name) // 使用person实例调用SetName方法,将person实例的字段name赋值为孙六
}
结构体标签
结构体标签是定义时以反引号 `` 包裹,写在字段类型后的键值对。
常用于传递元数据,本身不影响结构体的内存分配,也不可直接访问,通常是框架/库约定的标识符,如encoding/json包下的json。
使用结构体标签的字段需以大写字母开头提供包外访问的权限。
go
type User struct {
Name string `json:"username"` // 定义序列化后的字段名为username
Age int `json:"age"`
}
接口
接口本质是一组函数定义的集合,任何实现了接口中所有函数的类型都可以作为值赋给该接口类型的变量。
定义
go
type Calculate interface{
Add(a, b int) int // 加法
Sub(a, b int) int // 减法
Mult(a, b int) int // 乘法
Div(a, b int) float64 // 除法
}
组合
与结构体类似,接口也支持组合,但只支持匿名嵌套。
外层接口会包含所有内层接口定义的方法。
外层接口的实现类型必须实现包括内层接口定义函数的所有函数。
go
// 读接口
type Reader interface {
Read() string
}
// 写接口
type Writer interface {
Write(content string)
}
// 组合Reader和Writer,形成读写接口
type ReadWriter interface {
Reader // 嵌入Reader接口
Writer // 嵌入Writer接口
}
实现
若一个结构体类型实现了接口中定义的所有函数,那么该结构体就成为该接口的实现类型。
接口类型的变量,可以存储其实现类型的值。特别的,所有结构体类型都实现了空接口,所以空接口可以存储任何类型的值。
一个接口可以存在多个实现类型,一个结构体也可以实现多个接口。
接收者绑定的方式为接口的实现提供了相当高的灵活性。
go
// 接口定义
type Calculate interface{
Add(a, b int) int // 加法
Sub(a, b int) int // 减法
Mult(a, b int) int // 乘法
Div(a, b float64) (float64, string) // 除法
}
//结构体定义
type MathUtil struct{}
func (m MathUtil) Add(a, b int) int{
return a + b
}
func (m MathUtil) Sub(a, b int) int{
return a - b
}
func (m MathUtil) Mult(a, b int) int{
return a * b
}
func (m MathUtil) Div(a, b float64) (float64, string){
if b == 0 {
return 0.0, "除数不能为0"
}
return a / b, ""
}
func main() {
m := new(MathUtil) // 初始化结构体实例
result := m.Add(6, 7) // 调用Add方法
// 声明接口实例
var i Calculate
i = MathUtil{} // 将其实现类型赋值给接口
result2 := i.Sub(8, 3) // 使用接口调用
}
类型断言
此时我们产生了疑问,如果一个接口有很多实现类型,那么我们怎么知道该接口变量存储的是哪种类型的值呢?
Go提供了一种语法,能够获取接口的底层类型,即类型断言。
go
// ok 为true,value为类型的值
// ok 为false,value为零值,断言失败
value, ok := 接口变量.(具体类型)
协程与多路监听
由于本篇为入门篇,这里只讲述基本用法。
协程
在Go语言中,协程通常指goroutine。一种轻量级线程(初始栈大小为2KB),由Go运行时管理。
开启一个goroutine也非常简单,只需在函数调用前加上go关键字。
go
func save(user User){
// 将user存入数据库(耗时操作)
}
// main函数也运行在一个goroutine中,通常称为主goroutine
func main(){
// 省略获取user代码
// 开启另一个goroutine异步执行save操作
go save(user)
return
}
多路监听
想象一下以下场景:现在有多个通道,当某个通道执行完操作后需要执行一段代码逻辑。
很容易想到的是,可以开启多个for循环,对通道的状态进行持续判断,一旦通道变为期望的状态,结束循环并执行相应代码。
但是,当通道的数量非常多时,这样处理未免麻烦。
Go语言就提供了select关键字以简化处理。
go
// 模拟任务1:1秒后返回结果
func task1(ch chan<- string) {
// 调用time包下的公开函数Sleep,将当前goroutine阻塞
time.Sleep(1 * time.Second)
ch <- "任务1完成"
}
// 模拟任务2:500毫秒后返回结果
func task2(ch chan<- string) {
time.Sleep(500 * time.Millisecond)
ch <- "任务2完成"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// 开启两个goroutine异步执行任务
go task1(ch1)
go task2(ch2)
// 用select等待两个channel,谁先就绪就处理谁
select {
case res := <-ch1:
fmt.Println("收到:", res)
case res := <-ch2:
fmt.Println("收到:", res)
}
// 输出:收到:任务2完成(因为task2更快)
}
模块与包
Go语言使用模块(module)与包(package)来组织项目的架构。
通常一个项目就是一个模块,模块下面可以有很多的包,包下可以有很多的go文件,如图所示:

包的声明与导入
package关键字用于包的声明
go
package main // 声明当前go文件属于main包下
import关键字用于包的导入
go
improt (
"fmt" // 内置包,常用于字符串的格式化输出,是format的缩写
"net/http" // net是模块名,http是包名,以/分隔
)
- 导入包必须至少使用一次,否则编译报错
主程序入口
Go使用main包下的main函数作为程序的入口,main函数无参无返回值
go
package main
func main(){
// 你的代码
}
写在最后
作者水平有限,如有错误,敬请指正
倘若该文章有帮助到你,点一点赞和关注🤗