go基本语法总结,重要性不言而喻!
目录
一、数据类型的分类与创建方式
1.分类
1.1 按复杂程度划分
基本类型 | 复杂类型 |
---|---|
整数型、浮点型、bool、字符串 | 指针、函数、数组、切片、map、结构体、接口、管道 |
1.2 按传递类型划分
值类型 | 引用类型 |
---|---|
基本类型、函数、数组、结构体 | 指针、切片、map、接口、管道 |
其中函数的类型有争议,个人认为是特殊值类型,因为定义时内存中存的是值,调用时使用的是值,赋值后使用的是引用。
1.3 整数型分类
默认类型 | 有符号 | 无符号 |
---|---|---|
int, uint, rune(int32), byte(uint8) | int8, int16, int32, int64 | uint8, uint16, uint32, uint64 |
其中int,uint的大小取决于操作系统位数,32位就是32位,64位就是64位。Unicode码(任意字符)通常用rune存放,ASCII码通常用byte存放。
1.4 浮点型分类
单精度 | 双精度 |
---|---|
float32 | float64 |
小数常量默认为float64。高精度转低精度会有精度损失,值会改变。
2.创建方式(以具体实例演示)
类型 | 创建方式1 | 创建方式2 | 创建方式3 |
---|---|---|---|
整数型 | var i int;i = 1 | var i int = 1 | i := 1 |
浮点型 | var f float64;f = 1.0 | var f float64 = 1.0 | f := 1.0 |
bool | var b bool;b = true | var b bool = true | b := true |
字符串 | var s string;s = "1" | var s string = "1" | s := "1" |
指针 | i := 1;p := &i | p := new(int) | |
函数 | func test(a int) int {return 1} | f := func(a int) int {return 1} | |
数组 | arr := [3]int{1, 2, 3} | arr := [...]int{1, 2, 3} | arr := [...]int{0: 1, 1: 2, 3: 4} |
切片 | arr := [4]int{10, 20, 30, 40}; slice := arr[1:4] | slice := make([]int, 3,5) | slice := []int{1, 2, 3, 4, 5} |
map | mp := make(map[string]int, 5); mp["语文"] = 70 | mp := map[string]int{ "语文" : 70,"数学" : 80} | |
管道 | ch := make(chan int, 3) |
结构体
package main
import "fmt"
type Person struct {
Name string
Age int
Sex string
}
func main() {
var person1 Person
person1.Name = "李华"
person1.Age = 30
person1.Sex = "女"
fmt.Println(person1)
person2 := Person{"王伟", 25, "男"} //顺序赋值
fmt.Println(person2)
person3 := Person{Age: 28, Name: "张敏", Sex: "女"} //乱序赋值
fmt.Println(person3)
}
接口
package main
import "fmt"
//接口
type SayHello interface {
Say() //声明方法
}
//接口的实现类型,这里是结构体
type Chinese struct {
}
type American struct {
}
//实现接口方法
func (c Chinese) Say() {
fmt.Println("你好")
}
func (a American) Say() {
fmt.Println("hello")
}
func main() {
ch, am := Chinese{}, American{} //多态
ch.Say()
am.Say()
}
二、程序控制结构
1.分支结构
1.1 if else
语法:
if 条件1{
//语句1
}else if 条件2{
//语句2
}...
else if 条件n-1{
//语句n-1
}else{
//语句n
}
注意:条件表达式中可以定义变量,但只能用短变量声明,且后面必须跟分号。例:
if age := 20; age > 18 {
fmt.Println("你已成年")
}
1.2 switch
语法:
switch 表达式{
case 表达式1,表达式2,...:
语句块1
case 表达式3,表达式4,...:
语句块2
...
default:
语句块n
}
注意:switch后面可以不跟表达式,当作if else分支来处理。
go专属机制:
1.switch穿透:fallthrough
,fallthrough用于使当前case执行完后还能继续执行下一个case,必须放在case末尾且只能有一个,只能穿透一层。
2.type swich:类型推断,用于判断某个接口变量中实际指向的变量类型,语法:
switch 变量名 := 接口变量.(type) {
case 类型1:
语句块1
case 类型2:
语句块2
...
default:
语句块n
}
2.循环结构
语法:
//语法1
for 循环变量初始化;循环进行条件;循环变量迭代{
//执行语句
}
//语法2
for 循环进行条件{
//执行语句
}
//语法3
for {
//执行语句
}
//语法4,for-range
for 索引变量名, 值变量名 := range 变量{
//执行语句
}
注意:
1.语法3如果没有break就是死循环,所以要加上break(有协程的情况除外)。
2.用for-range遍历字符串得到的value是Unicode码值,需要用%c才能显示原字符。
3.用for-range遍历管道,只有一个返回值value,没有index。
三、重要概念辨析
1.init函数,匿名函数,闭包
每个文件都可以定义init函数,它会在main函数执行前被调用,用于初始化。init的优先级:文件之间,被导包>当前包,文件内,全局变量>init>main。匿名函数就是没有名字的函数,用于变量初始化,其形式已在第一节讲过。使用匿名函数最需要注意的是:不加括号返回的是匿名函数的引用,加括号就是调用一次匿名函数。如果匿名函数被用于协程,是一定要加括号的,因为匿名函数要作为轻量级线程,就是动态的程序概念,所以一定要调用。匿名函数作为协程还要特别注意多协程的情况,因为在多协程中,如果协程要使用循环变量,那么循环变量会和每个协程(匿名函数)形成闭包,使得循环变量一直留于内存,被所有协程引用。所以闭包的本质是匿名函数,特点是引用外部变量,使其一直留于内存。
2.方法与函数的区别
方法 | 函数 | |
---|---|---|
和其他类型的关系 | 绑定到特定类型 | 独立于任何类型 |
语法 | func (receiver ReceiverType) MethodName(parameters) returnType | func FunctionName(parameters) returnType |
调用方式 | 通过类型的实例调用 instance.MethodName() | 直接调用 FunctionName() |
作用域 | 可以访问绑定类型的字段 | 只能访问传入的参数 |
指定类型是否需要和传入类型一致 | 不需要(可以随意传入值或指针) | 需要 |
3.切片与数组的区别
1.数组的大小是固定的,在定义时就必须指定,不能动态改变。而切片是动态的,可以根据需要扩展。
2.数组是值类型,传递数组时会复制整个数组。切片是引用类型,传递的是指向底层数组的指针,因此切片的操作会影响原始数据。
3.数组在声明时分配内存,其大小固定。切片在运行时可以根据需要动态分配内存,并且可以通过append函数增加元素,go会自动处理内存分配。
4.封装,继承,多态
这是面向对象的三大特性。封装就是把字段及其操作方法封装在一起,被封装的字段无法被外部调用程序直接访问,只有通过调用指定的方法才能对字段进行操作,提高了数据的安全性。继承就是一个或多个类(GO中是结构体)拥有另一个类(GO中是匿名结构体)的所有字段和方法,这种关系称为继承,被继承的类称作父类,继承类称作子类。子类不仅可以使用父类的字段和方法,还可以自定义父类没有的字段和方法,提高了代码的复用性。多态就是一个类型拥有不同的特征,这种特征在GO中通过接口实现。具体来说,一个接口中的方法可以被多个类型实现,使该接口呈现出不同类型的特征。这样,只需依赖该接口定义的行为,代码就可以处理不同类型的对象,而不需要了解它们的具体类型,提高了代码的灵活性。
5.协程与管道
这是GO的核心,也是GO流行的主要原因,有了这两个机制,GO就能高效处理高并发行为。协程是一种线程,它能将自己的IO操作最大限度地隐藏起来,从而可以迷惑操作系统,让其将更多的cpu执行权限分配给自己。注意:线程是CPU控制的,而协程是程序自身控制的,属于用户级别的切换,操作系统感知不到,所以说协程是轻量级线程。协程如何将自己的IO操作最大限度地隐藏起来?原理是GO让协程和主线程并发执行,大大提高了程序的执行效率,而且由于读写锁机制,协程便能更高效地应对读操作数量远大于写操作的场景。但数据传输的安全性也很重要,所以GO引入了管道机制,管道的本质是一个队列,因其先进先出的特性,保证了多个协程操作同一个管道时,不会发生资源抢夺问题。
6.断言
在GO中,接口支持多态(即同一接口变量可以存储不同的具体类型的值)。通过类型断言,可以根据需要获取接口变量所存储的具体类型,从而访问特定的属性和方法。当需要处理多种类型时,类型断言可以与switch语句结合使用,成为类型选择,从而简化对多个类型的处理,以下是多态实例:
func Greet(s SayHello) {
s.Say()
switch person := s.(type) {
case Chinese:
person.NiuYangGe()
case American:
person.Disco()
default:
fmt.Println("未知类型")
}
}
为了避免因断言失败而引发的panic,还可以使用安全类型断言,通过获取第二个返回值来判断断言是否成功:value, ok := interfaceVariable.(ConcreteType)
7.反射
反射的主要作用是提供动态操作的能力。它允许程序在运行时检查类型、调用方法、访问和修改字段等,而不是在编译时静态确定。这对以下场景特别有用:
(1)动态类型检查和处理:当程序需要处理未知类型或接口时,反射可以获取类型信息并做出相应处理。
(2)序列化和反序列化:反射可用于自动将数据结构转换为JSON/XML等格式,或者反过来,将这些格式的数据转换为GO结构体。
(3)通用库的实现:像fmt包中的Printf就使用了反射来处理变长参数,支持各种类型。
(4)动态调用方法:在运行时根据输入决定调用哪个方法,而不需要硬编码。
8.错误处理
错误处理是一种显式的方式,函数通常会返回一个error类型值,调用者需要检查这个返回值来判断是否发生了错误。这样可以灵活地处理错误情况,并且控制权完全交由调用者。
defer+recover机制用于从运行时恐慌(panic)中恢复。defer延迟执行某个函数,直到包含它的函数返回,通常用于资源释放等场景。当程序出现严重错误导致panic时,recover可以捕获panic,避免程序崩溃,从而优雅地恢复并继续执行代码。这在处理不可预见的错误时非常有用。
9.Kind()和TypeOf()的区别
特性 | reflect.TypeOf() | reflect.Kind() |
---|---|---|
返回值 | 返回 reflect.Type 类型的对象 | 返回 reflect.Kind 类型的枚举值 |
作用 | 获取变量的完整类型信息 | 获取变量的基本种类(如 int 、string 、struct ) |
适用场景 | 当需要获取类型的详细信息时 | 当只需要判断数据类型的基本特性时 |
四、重要类型的操作
1.切片
package main
import (
"fmt"
)
func main() {
// 1. 切片的初始化和修改
slice := []int{1, 2, 3, 4}
fmt.Println("初始切片:", slice)
// 修改切片元素
slice[0] = 10
fmt.Println("修改后的切片:", slice)
// 2. 添加元素导致扩容
fmt.Printf("初始容量: %d\n", cap(slice))
// 添加元素,超出容量
slice = append(slice, 5, 6, 7, 8)
fmt.Println("添加元素后的切片:", slice)
fmt.Printf("添加元素后的容量: %d\n", cap(slice)) // 容量自动扩容
// 3. 遍历切片
fmt.Println("遍历切片:")
for i, v := range slice {
fmt.Printf("索引: %d, 值: %d\n", i, v)
}
// 4. 切片继续切片
subSlice := slice[2:5]
fmt.Println("子切片:", subSlice)
// 5. 切片拷贝
anotherSlice := make([]int, len(subSlice)) // 创建一个和子切片长度一样的新切片
copy(anotherSlice, subSlice)
fmt.Println("拷贝后的新切片:", anotherSlice)
// 修改拷贝后的切片,不影响原切片
anotherSlice[0] = 100
fmt.Println("修改后的新切片:", anotherSlice)
fmt.Println("原切片保持不变:", subSlice)
}
2.map
package main
import (
"fmt"
)
func main() {
// 1. 创建并初始化 map
myMap := map[string]int{
"apple": 5,
"banana": 3,
"orange": 7,
}
fmt.Println("初始 map:", myMap)
// 2. 增加/更新元素
myMap["grape"] = 4 // 增加
myMap["banana"] = 10 // 更新
fmt.Println("增加/更新元素后的 map:", myMap)
// 3. 删除元素
delete(myMap, "orange")
fmt.Println("删除 'orange' 后的 map:", myMap)
// 4. 查找元素
value, exists := myMap["apple"]
if exists {
fmt.Println("'apple' 存在,值为:", value)
} else {
fmt.Println("'apple' 不存在")
}
// 查找一个不存在的元素
_, exists = myMap["melon"]
if !exists {
fmt.Println("'melon' 不存在")
}
// 5. 遍历 map
fmt.Println("遍历 map:")
for key, value := range myMap {
fmt.Printf("键: %s, 值: %d\n", key, value)
}
// 6. 清空 map - 方法1:使用 delete 逐个删除
for k := range myMap {
delete(myMap, k) // 逐个删除
}
fmt.Println("使用 delete 清空后的 map:", myMap)
// 7. 清空 map - 方法2:使用 make 创建一个新的 map
myMap = make(map[string]int) // 原来的 map 将被垃圾回收
fmt.Println("使用 make 创建新 map 后的 map:", myMap)
}
3.管道
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个缓冲通道
chan1 := make(chan int, 1)
chan2 := make(chan string, 1)
// 启动 goroutine 向 chan1 写入数据
go func() {
time.Sleep(time.Second * 1)
chan1 <- 1
}()
// 启动 goroutine 向 chan2 写入数据
go func() {
time.Sleep(time.Second * 2)
chan2 <- "hello"
}()
// 持续监听通道
for {
select {
case v := <-chan1:
fmt.Println("intchan:", v) // 如果 chan1 被写入,打印数据
return // 读取后退出循环
case v := <-chan2:
fmt.Println("stringchan:", v) // 如果 chan2 被写入,打印数据
return // 读取后退出循环
default:
fmt.Println("防止阻塞") // 如果没有通道可读,打印该信息
// 等待一段时间,防止立即进入下一循环而输出过多信息
time.Sleep(500 * time.Millisecond)
}
}
}
五、所有重要细节整理
1.每个go文件必须有package声明且只能有一个。
2.局部变量或导入的包必须被使用。
3.同一目录下的所有go文件必须属于同一个包,即同一目录下的所有go文件必须都作一样的package声明。
4.全局变量必须用var关键字定义,定义后可以不使用。
5.未定义数组元素部分采用该元素类型的默认值,比如int数组采用索引赋值法,未赋值的元素默认为0。
6.字符的本质是整数,和int运算时,按照其unicode码值运算。
7.字符串可以用下标访问,但无法通过下标修改。
8.字符串的拼接换行时,加号必须留在上一行末尾。
9.字符串是以字节形式存储的,当存在超过1个字节大小的字符时,遍历字符串要转成rune切片或用for-range。
10.for-range会将字符串视作Unicode字符(即一个rune)的集合进行遍历,所以每次迭代时,从字符串中提取一个rune。
11.当整数型或浮点型存储超出其大小的数字时会溢出报错,但强转的变量即使数值溢出也不会报错。
12.不同类型的变量之间必须强转才能赋值,type定义的类型和原类型之间也要强转。
13.函数名或全局变量名的首字母如果小写,则只能在同包内使用,无法跨包使用。
14.无论是分支结构还是循环结构,单个执行语句都要用花括号包裹。
15.for和switch后面都可以不跟表达式。
16.当返回列表存在且声明了函数内的变量名时,若只写return,那么返回变量的顺序由返回列表的变量名决定,若写了return,则return的优先级高于返回列表。
17.defer会将函数或函数调用语句压入一个栈中,再按栈的顺序取出栈顶元素,函数或调用语句中的变量的值也会一起保存。
18.recover必须在defer函数中调用才能捕获到恐慌,否则返回nil。
19.make只用于切片,map,管道,其中管道必须通过make初始化。
20.map中的key必须是可比较类型,所以不能是切片,映射,函数,管道。
21.go编译器会自动处理结构体指针,所以不仅能用.访问和修改结构体指针指向的内容,方法参数要求是结构体指针或值时,可以传入结构体值或指针。
22.结构体和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)。
23.方法中的receiver的类型只能是自定义类型(结构体或某个类型的别名)。
24.若一个类型定义了其方法,则其他类型不能调用这个方法。
25.如果某个自定义类型实现了返回string,名字为String()的方法(如果是同包内调用,s可以小写),那么fmt包的Printf或Println函数打印该类型时,会自动调用String()。
26.子类和父类中的字段冲突,使用子类对象时优先用子类的字段,如果仍然想用父类的,那么字段访问时就要显示调用父类名:子类.父类.字段。
27.结构体的字段可以是结构体和匿名结构体的组合。二重结构体除了访问时还可以再“点”一下,和其他类型没什么区别,当然,二重结构体不是继承。
28.接口中只能包含方法和接口,不能包含变量。
29.实现接口必须实现其所有方法才算实现。
30.和方法一样,只要是自定义数据类型,就可以实现接口,一个自定义类型可以实现多个接口。
31.接口本身不能创建实例,但可以指向一个实现了该接口的变量(如结构体)。
32.一个接口可以继承多个接口,这时如果要实现该接口,必须将被继承接口的方法都实现。
33.空接口可以接收所有类型,未实现的非空接口除外。
34.管道遍历前一定要关闭,否则会发生死锁。
35.只有go关键字放主线程前,GO才会使协程和主线程并发执行。
36.主线程一旦结束,如果之后没有同步机制或睡眠等多余的操作,协程会立即结束,无论它是否执行完。
37.主线程和协程在宏观上是并行执行的,微观上是通过不同的时间片交替执行的,而且交替顺序未必是固定的,即并发执行。
38.管道用<-读数据和写数据,但这里的”读“是取出数据,”写“是存入数据。
39.空管道或满管道都会造成阻塞。
40.管道关闭后,就不能向它写数据了,但可以读数据。
41.默认情况下,管道是可读可写的,但可以声明为只读或只写。
42.select选取的仅是准备好的通道,只有多个通道都准备好才会随机选取,否则只选先准备好的通道。
43.切片的索引区间是左闭右开的。