MyGO!!! (速通Golang基础)
寒假打了Hgame,发现web基本给的都是go文件,就想着速通一下,至少能看懂(),本篇非零基础(即之前学过其他语言),部分不解释原理,有缺漏或者建议欢迎补充和提出
参考8小时转职Golang工程师(如果你想低成本学习Go语言)_哔哩哔哩_bilibili
原理图源来自上述视频(侵删),对视频做了整理和补充()
1. Hello World
package main
import "fmt" // (导入fmt头文件)
func main(){ //注意这个大括号不能分段,只能同一行,这是go的规定
fmt.Println("hello world")
}
1.什么是 package
?
- 在Go语言中,
package
是代码组织的基本单位,类似于其他语言中的“模块”或“模块化代码” - 文件是以包的形式分组的。每个源文件都属于一个包,并且每个包可以包含多个文件
- 包声明的作用是声明当前文件所属的包
2.package main
的作用
- 程序的入口点:当声明了一个
package main
时,这个Go程序会被编译成一个可执行文件。换句话说,只有在package main
中定义的代码才会成为独立运行的程序 - 包含
main
函数:package main
是唯一一个可以定义main
函数的包。main
函数是程序的起点,程序从这里开始执行 - 全局唯一性:在一个Go项目中,只能有一个
package main
,它决定了程序的入口
2. 常见的变量声明
1.单变量
1.var a int = 0 //go语言中类型是在变量名后面的,赋值则默认值是0,但建议先赋值,不然vscode里可能给你弹 //提示,以及定义和赋值建议在同一行
2.var num = "type" //类型推导:自动判断类型
3.num := 555 //合并写法,注意是 := (常用方法) 但只能用在函数体内,不能全局
2.多变量声明
1.var n1,name,n3 = 100,"name",888
2.n1,name,n3 := 100,"tom",888
3.var xx,yy int = 100,200 //同类型
4.var (
vv int = 100
jj bool = true
)
3. const与const() 中的iota
1.const
定义一个变量不能被修改(常量)
const a int = 10
const(
a = 10
b = 20
)
2.iota
//const 用来定义枚举类型
//可以在const()中添加关键字iota,默认为0,每行都会加一
const(
beijing = iota //0
shanghai //1
gaungzhou //2
shenzhen //3
)
iota只能配合const()使用,不能在其他地方使用,但我个人觉得没啥用
4. 函数多返回值
1.单返回
func re(a string , b int) int{ // 最后int是返回类型
fmt.Println("a =",a)
fmt.Println("b =",b)
c := 100
return c
}
c := re("aaa",555)
fmt.Println("c =",c)
// a = aaa b = 555 c = 100
2.多返回
匿名返回
func res2(a string , b int) (int ,int){
fmt.Println("a =",a)
fmt.Println("b =",b)
c := 100
d := 777
return c,d
}
ret1,ret2 := res2("GenshinImpact",2021)
fmt.Println("ret1 =",ret1,"re2 =",ret2)
//ret1 = 100 ret2 = 777 a = GenshinImapact b = 2021
有形参名字
func foo3(a string, b int) (r1, r2 int){ // (r1 int, r2 int)
fmt.Println("r1 =",r1)
fmt.Println("r2 =",r2)
//r1,r2属于foo3的形参,初始化默认为0
//作用域为foo3这个函数体的{}空间
r1 = 1000
r2 = 2000
return
}
5. 指针
与其他语言同理
package main
import (
"fmt"
)
func swap(a *int,b *int){
var temp int = 0
temp = *a
*a = *b
*b = temp
}
func change(p *int){
*p = 10
}
func main(){
fmt.Println("Second One")
a := 1
change(&a) //(带取址符号&传入)
b := 555
c := 777
swap(&b,&c) //同上
fmt.Println("a =",a)
fmt.Println("b =",b,"c =",c)
}
//Second One
//a = 10
//b = 777 c = 555
6. Defer
1.defer的顺序
defer
的基本作用
- 延迟执行:
defer
会将函数调用保存到一个栈中,直到当前函数执行完毕,并准备返回之前才会依次执行这些被延迟的函数调用 - 执行顺序:多个
defer
语句会按照 后进先出(LIFO) 的顺序执行 - 清理操作:常用于资源释放、文件关闭、锁解锁等场景,以确保某些操作在函数结束时被执行
package main
import "fmt"
func main(){
defer fmt.Println("main end1")
defer fmt.Println("main end2")
fmt.Println("It is the first")
fmt.Println("Second one")
}
//It is the first
//Second one
//main end2
//main end1
这里可以看出defer是在最后执行的,而且先执行的defer排在第二个之后
(图源:上文视频,水印勿看)
总结来说defer是个先进后出的调用方式
2.defer 和 return 顺序
package main
import (
"fmt"
)
func defFunc() int {
fmt.Println("defer !!!")
return 0
}
func returnFunc() int {
fmt.Println("return !!!")
return 0
}
func returnA() int {
defer defFunc()
return returnFunc()
}
func main(){
returnA()
}
//return !!!
//defer !!!
这里说明defer 和 return 在同一个函数中的话,defer是比return后执行的,因为defer是在当前函数的生命周期结束之后才被执行(到函数最后一个大括号)
7. For循环
在Go语言中,for
循环是唯一支持的循环结构,以下是for
循环的常见用法:
1.基本形式
for 初始化; 条件; 后操作 {
// 循环体
}
示例:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
运行结果:
0
1
2
3
4
2.无限循环
for {
// 无限循环
}
示例:
package main
import "fmt"
func main() {
for {
fmt.Println("Loop forever!")
}
}
3.类似 while
的用法
for 条件 {
// 循环体
}
示例:
package main
import "fmt"
func main() {
x := 0
for x < 5 {
fmt.Println(x)
x++
}
}
4.类似 do-while
的用法
Go语言没有 do-while
语句,但可以通过控制逻辑来实现:
func main() {
x := 0
for {
fmt.Println(x)
x++
if x >= 5 {
break
}
}
}
5.控制循环
break
:跳出当前循环continue
:跳过当前循环的剩余部分,继续下一次循环
示例:
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
break // 跳出循环
}
if i%2 == 0 {
continue // 跳过偶数
}
fmt.Println(i)
}
}
6.使用 range
遍历
range
是 Go 中用于循环遍历切片、数组、字符串和字典的关键字
index
: 表示元素的索引位置(从 0 开始)- 示例中,
index
的值分别为 0、1、2、3、4
- 示例中,
value
:表示当前索引对应的值- 示例中,
value
的值分别为 10、20、30、40、50
- 示例中,
遍历数组/切片:
package main
import "fmt"
func main() {
arr := []int{1, 2, 3, 4, 5}
for index, value := range arr {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
}
运行结果:
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Index: 3, Value: 4
Index: 4, Value: 5
遍历字符串:
package main
import "fmt"
func main() {
str := "Hello"
for index, char := range str {
fmt.Printf("Index: %d, Character: %c\n", index, char)
}
}
遍历字典:
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
for key, value := range m {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
}
7.总结
for
:通用循环结构range
:用于高效遍历集合break
/continue
:控制循环流程
8. 数组和动态数组
1.定义
var arr [5]int // 定义一个包含5个整数的数组,初始值为零值(0)
var arr [3]string // 定义一个包含3个字符串的数组,初始值为空字符串("")
初始化:
arr := [3]int{10, 20, 30} // 初始化一个包含3个整数的数组
arr := [...]string{"apple", "banana", "cherry"} // 使用 `...` 表示根据元素自动推断数组长度
而也可以只定义前几个数字:
package main
import "fmt"
func main(){
arr := [10]int{1,2,3,4}
for index,value := range arr{
fmt.Println("index =",index,"value =",value)
}
}
index = 0 value = 1
index = 1 value = 2
index = 2 value = 3
index = 3 value = 4
index = 4 value = 0
index = 5 value = 0
index = 6 value = 0
index = 7 value = 0
index = 8 value = 0
index = 9 value = 0
注意:
- 数组的长度是其类型的一部分。例如,
[3]int
和[5]int
是不同的类型 - 数组的长度是固定的,无法动态调整大小
2.动态数组(slice)
切片是动态数组,它是一个引用类型,底层基于数组实现,可以动态调整大小
定义语法:
var slice []int // 定义一个空的切片,初始值为 `nil`
slice := make([]int, 5) // 创建一个长度为5的切片,初始元素为零值
slice := []string{"a", "b", "c"} // 直接初始化切片
if slice == nil{
fmt.Println("slice为空")
} else { // 不能分段
fmt.Println("slice有空间")
}
//可以用这个来验证slice初始值
package main
import "fmt"
func printarr(arr []int){
//引用传递
// _ 表示匿名变量
for _, value := range arr{
fmt.Println("value =",value)
}
arr[0] = 100
}
func main(){
arr := []int{1,2,3,4}
fmt.Printf("arr type is %T\n",arr) //Printf支持格式化字符串,可以精确控制输出的格式。
//例如,%d 表示整数,%s 表示字符串,%f 表示浮点数等。
printarr(arr)
fmt.Println("==========")
for _, value := range arr{
fmt.Println("value =",value)
}
}
/*
arr type is []int
value = 1
value = 2
value = 3
value = 4
==========
value = 100
value = 2
value = 3
value = 4
*/
也可以动态调整数组大小:
-
追加元素:
arr = append(slice, 10, 20, 30) // 向切片追加元素
在此之前先介绍容量(cap):
package main
import "fmt"
func main(){
var numbers = make([]int ,3, 5) //长度三容量五初始为0的数组
fmt.Println("len = ",len(numbers),"cap = ",cap(numbers),"slice = ",numbers)
}
(图源:上文视频,水印勿看)
这样追加之后numbers = append(numbers , 1)
长度就会变成4,但是容量还是5
numbers = [0,0,0,1]
如果超出容量的话,就会再开辟一个cap容量(5),就是你设置的容量
(图源:上文视频,水印勿看)
-
切片操作:
sub := arr[1:4] // 创建一个从索引1到3(不包括4)的子切片
如果存在:s1 := s[0:2]
那么如果执行s1[0] = 100
后
s1和s都会被改变(因为底层数组相同)
简单示例:
package main
import "fmt"
func main() {
// 定义一个切片
slice := []int{10, 20, 30}
// 追加元素
slice = append(slice, 40, 50)
// 打印切片
fmt.Println(slice) // 输出:[10 20 30 40 50]
// 切片操作
sub := slice[1:4]
fmt.Println(sub) // 输出:[20 30 40]
}
动态数组里还有两个重要的概念:
1. map
的含义
map
是 Go 语言中的一种动态数组,也称为“字典”或“哈希表”- 它允许你存储键值对(Key-Value),并通过键快速查找对应的值
map
的大小是动态调整的,可以根据需要增长或缩小
语法定义:
var m map[KeyType]ValueType
创建 map
的方式:
// 使用 `make` 创建
m := make(map[string]int)
// 直接初始化
m := map[string]int{"apple": 5, "banana": 10}
(单纯输出的话是乱序,但如果是遍历输出的话就是按顺序)
操作 map
:
// 插入或更新键值对
m["cherry"] = 20
// 获取值
value, exists := m["apple"]
// 删除键值对
delete(m, "banana")
// 遍历 `map`
for key, value := range m {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
注意:
如果一开始map不作初始化的时候相当于一个空指针,需要make创建空间,直接赋值就不需要
2. make
的含义
make
是一个内置函数,用于创建集合类型(如切片[]
、map
和通道chan
)- 它为这些动态数据结构分配内存并返回一个引用
语法:
var variable_name = make(Type, arguments...)
常见用法:
// 创建一个切片
slice := make([]int, 5) // 长度为5,容量为5的切片
// 创建一个 `map`
m := make(map[string]int, 10) // 提前分配空间,但长度为0
// 创建一个通道
ch := make(chan int, 100) // 有缓冲的通道,容量为100
总结:
map
是一种动态数组,用于存储键值对,通过键快速查找值make
是用于创建map
、切片和通道的函数,为它们分配内存
9. Struct基本定义和使用
1.type关键字
定义一个类型别名:(类似C语言中的typedef,但这里的type是创建新的类型,并非只提供别名)
- 定义自定义类型:可以基于内置类型或结构体来定义新的类型,使其具有特定的特性
- 定义结构体:用
type
来定义复杂的自定义数据结构
package main
import "fmt"
// 定义一个类型别名
type ID int
func main() {
var id ID = 100
fmt.Println(id) // 输出: 100
fmt.Printf("%T\n", id) // 输出: main.ID
fmt.Printf("%T\n", int(id)) // 转换为 int 类型
}
2.结构体定义
package main
import "fmt"
// 定义一个结构体
type Person struct {
name string
age int
}
func main() {
// 创建结构体实例
p := Person{
name: "Alice",
age: 30,
}
fmt.Println(p) // 输出: {Alice 30}
}
也可以尝试另一种定义方式:
var person Person
persion.name = "Furina"
persion.age = 500
fmt.Println(persion)
如果定义一个函数要改变它的值:(传地址)
10. 面向对象类的表示和封装
Go 语言不像 Python 或 Java 那样有传统的类(Class)概念,Go 确实在语法层面没有直接支持类,但可以通过 结构体(struct) 和 方法(method) 的组合来实现类似面向对象编程(OOP)的行为,比如封装、继承和多态
1.定义结构体
type Game struct{
name string
year int
enterprise enterprise //嵌套结构体
}
type enterprise struct{
city string
name_f string
}
再创建实例:
g := Game{
name : "zzz",
year : 2024,
enterprise: enterprise{
city: "ShangHai",
name_f: "miHoYo",
},
}
2.方法
在 Go 中,方法是绑定到某个类型(通常为结构体)上的函数。方法通过在函数声明前加上 receiver
(接收者)来指定所属的类型
func (g *Game) SayHello(){
fmt.Println("Tech otakus save the world")
fmt.Println("Welcome to ",g.name,",publishing in ",g.year)
}
func (g Game) GetName_Publish() string{
return g.enterprise.name_f
}
在这之中,方法接收者为*Game和Game
而至于为什么一个有*而一个没有呢
就是*的指针接收者可以修改原始Game的实例,下面的值接收者就不会改变实例,具体怎么用看实际需求
调用:
g.SayHello()
name := g.GetName_Publish()
fmt.Println(name)
完整如下:
3.封装
Go 中通过控制字段和方法的可见性来实现封装。字段和方法的名称首字母大小写决定了它们的可见性:
- 小写字母开头:只能在当前包(package)内部访问
- 大写字母开头:可以在其他包(package)中访问
type Person struct {
name string // 私有字段
age int // 私有字段
city string // 私有字段
}
// 提供一个公有的方法来访问私有字段
func (p *Person) GetAge() int {
return p.age
}
// 提供一个公有的方法来设置私有字段
func (p *Person) SetAge(age int) {
p.age = age
}
11. 继承
定义一个新的结构体里面嵌套父级结构体:
type Ten struct{
Game //继承了父类Game
level int
}
创建一个新的实例:
s := Ten{Game{"starrail",2023,enterprise{"shanghai","mihoyo"}},5}
如果觉得这样太麻烦了,可以var s Ten
然后再逐一赋值s.name = "???"
创建方法:
func (s *Ten) Fly(){
fmt.Println("Heads up ! The tracks are running !")
}
func (s *Ten) Sing(){
fmt.Println("The wheels are singing !")
}
调用方法:
s.SayHello()
s.Fly()
s.Sing()
fmt.Println(s)
/*
输出:
Welcome to starrail , publishing in 2023
Heads up ! The tracks are running !
The wheels are singing !
{{starrail 2023 {shanghai mihoyo}} 5}
*/
12. 对象多态实现
1.Go语言中的多态
多态的定义
多态是面向对象编程(OOP)的三大特性之一,另外两个是封装和继承。在编程中,多态指的是同一个接口或方法可以有不同的实现方式。在Go语言中,多态主要通过 接口 和 类型断言 来实现
通过接口实现多态
Go语言中的接口是一种 隐式实现 的方式,不需要显式的继承或实现声明。只要类型实现了某个接口的所有方法,即可视为实现了该接口。这种机制使得不同的类型可以遵循相同的接口规范,从而实现多态行为
2.接口
接口是包含方法签名的集合。它定义了对象应该具有的行为,而不需要关心对象的具体实现细节。在 Go 中,接口是一种抽象类型,不能直接实例化
简单定义:
接口通过 type
关键字和 interface
类型定义
type Animal interface{ //本质是一个指针
Sleep()
GetColor() string //获取动物颜色
GetType() string //获取动物种类
}
简单实现:
在 Go 中,类型(如结构体)通过实现接口定义的所有方法来“隐式”满足接口。不需要显式声明“实现某个接口”,只要类型实现了接口的所有方法,它就被认为实现了该接口
type Dog struct{
color string
typedef string
}
func (p *Dog) Sleep(){
fmt.Println("Dog is sleeping")
}
func (p *Dog) GetColor() string{
return p.color
}
func (p *Dog) GetType() string{
return p.typedef
}
在这个例子中,Dog实现了Animal的接口,这样的话Dog可以被赋值为Animal类型的变量
func main(){
var animal Animal //接口数据类型
animal = &Dog{"black","labuladuo"}
animal.Sleep()
name := animal.GetColor()
fmt.Println(name)
}
/*
Dog is sleeping
black
*/
- 多态性:通过接口,可以在不同的实现中使用同一组方法,实现多态行为
- 解耦:接口将实现与接口分开,使代码更加灵活和可维护
- 抽象:接口提供了一种抽象的方式,隐藏了实现细节
怎么体现他的多态性?我们可以再定义一个Cat类型:
type Cat struct{
color string
typedef string
}
func (s *Cat) Sleep(){
fmt.Println("Cat is sleeping")
}
func (p *Cat) GetColor() string{
return p.color
}
func (p *Cat) GetType() string{
return p.typedef
}
然后我们让animal等于具体的Cat:
animal = &Cat{"Green","QQ"} //这里需要传址是因为interface本质上是一个指针
animal.Sleep() //调用猫的sleep
发现给他具体什么对象,就可以调用这个对象的方法,这样就触发了一种多态现象
另外还有一种触发方法:
func show(animal Animal){
animal.Sleep()
fmt.Println("color =",animal.GetColor())
fmt.Println("type =",animal.GetType())
}
func main(){
cat := Cat{"Green","QQ"}
dog := Dog{"Yellow","labuladuo"}
show(&cat)
show(&dog)
}
上面所展示的也是一样的效果
总结就是:
(图源:上文视频,水印勿看)
13. Interface空接口
1.空接口(interface{}
)
Go 中有一个特殊的空接口 interface{}
,它可以存储任何类型的值。任何类型都隐式满足空接口的要求,因为它没有任何方法。(通用万能类型)
2.类型断言
那么既然什么类型都能传进来,我们可以使用类型断言来判断接口变量实际类型,并获取值
func MyFunc(arg interface{}){
fmt.Println("Launching....")
fmt.Println(arg)
value, ok := arg.(string)
if !ok{
fmt.Println("arg is not string type ")
}else{
fmt.Println("value =",value)
}
}
也可以进行多个判断:
if value,ok := arg.(int) ok{
fmt.Println("int")
}else if value,ok := arg.(string) ok{
fmt.Println("string")
}else{
fmt.Println("unknown type")
}
基本语法
value, ok := interfaceVar.(Type)
参数解释:
interfaceVar
:一个接口类型的变量Type
:期望的类型value
:如果断言成功,返回Type
类型的值ok
:布尔值,表示断言是否成功
14. 变量内置pair结构
变量分为type和value,type和value就是一个pair
package main
import "fmt"
func main(){
var a string = "ace"
// pair<type:string,value:ace>
var AllType interface{}
AllType = a
str,_ := AllType.(string)
fmt.Println(str)
}
就是无论给a如果赋值,interface就只找到string类型去给str赋值
这段代码告诉我们,不管值怎么变,类型和值都是一个pair,绑定的
另外,如果给定:
b := &Book{}
var r Reader = b
那么b和r的pair是相同的,这表示变量之间相互传递pair不变
另一种:
package main
import "fmt"
type Type interface {
ReadBook()
}
type Writer interface{
WriteBook()
}
type Book struct{
}
func (b *Book) ReadBook(){
fmt.Println("reading....")
}
func (b *Book) WriteBook(){
fmt.Println("writing....")
}
func main(){
b := &Book{} // pair<type:Book,value:book{}地址>
var r Type = b // pair<type:Book,value:book{}地址>
r.ReadBook()
var w Writer = r.(Writer) //这次断言成功是因为w,r的具体type是一致(强制类型转换)
w.WriteBook()
}
可以发现pair都是不变的,实质上也算一种继承,因为Book同时实现了Type和Writer两个接口
15. Reflect 反射机制
在 Go 中,每个变量都有两个部分:类型(Type)和 值(Value)。反射允许你在运行时获取和操作这两个部分
- 类型:表示变量的数据类型,例如
int
、string
、struct
等 - 值:表示变量的实际数据内容,例如字符串
Hello
或整数42
Go 的反射机制主要依赖于以下两个核心方法:
reflect.TypeOf(v interface{})
:返回变量v
的类型reflect.ValueOf(v interface{})
:返回变量v
的值
包含头文件:“reflect”
1.用法
获取变量的类型和值:
package main
import (
"fmt"
"reflect"
)
func main() {
name := "Alice"
age := 30
// 获取类型和值
nameType := reflect.TypeOf(name)
nameValue := reflect.ValueOf(name)
ageType := reflect.TypeOf(age)
ageValue := reflect.ValueOf(age)
fmt.Println("Name Type:", nameType) // 输出: string
fmt.Println("Name Value:", nameValue) // 输出: Alice
fmt.Println("Age Type:", ageType) // 输出: int
fmt.Println("Age Value:", ageValue) // 输出: 30
}
加上遍历:
type User struct{
Id int
Name string
Age int
}
func (p User) Call(){
fmt.Println("calling....")
fmt.Println(p)
}
func main(){
user := User{1,"mi",18}
DoFileAndMethod(user)
}
func DoFileAndMethod(input interface{}){ // input形参
// 获取类型
inputType := reflect.TypeOf(input)
fmt.Println(inputType.Name())
//获取值
inputValue := reflect.ValueOf(input)
fmt.Println(inputValue)
for i := 0; i < inputType.NumField();i++{ //获取字段数量NumField()
field := inputType.Field(i) //获取第i个字段信息
value := inputValue.Field(i).Interface() //获取第i个字段的值
fmt.Printf("%s: %v = %v\n",field.Name, field.Type, value)
}
for i := 0;i < inputType.NumMethod();i++{ //注意,在这边要将Call改成接受值类型而非传址
m := inputType.Method(i) //或者也可以DoFileAndMethod(&user)
fmt.Printf("%s: %v\n",m.Name,m.Type)
}
}
/*
User
{1 mi 18}
Id: int = 1
Name: string = mi
Age: int = 18
Call: func(main.User)
*/
其中:(这些后缀都可以在对应头文件源码中找到,也可以通过https://studygolang.com/pkgdoc)
inputType.NumField()
NumField()
是reflect.Type
的一个方法,返回该结构体的字段数量
inputType.Field(i)
- 获取结构体中索引为
i
的字段 - 返回值是一个
reflect.StructField
类型,包含了字段的名称、类型、标签(struct tag
)等信息
inputValue.Field(i).Interface()
Field(i)
获取结构体中索引为i
的字段的值Interface()
方法将反射值(reflect.Value
)转换回接口类型(interface{}
),以便可以获取该值的实际类型和值
inputType.NumMethod()
NumMethod()
是reflect.Type
的一个方法,返回该类型的方法数量
inputType.Method(i)
- 获取结构体中索引为
i
的方法 - 返回值是一个
reflect.Method
类型,包含了方法的名称、类型等信息
2.动态设置变量的值:
func main() {
a := 5
v := reflect.ValueOf(&a).Elem() // 获取变量 a 的反射值
v.SetInt(10) // 修改变量 a 的值为 10
fmt.Println("a is now:", a) // 输出: a is now: 10
}
3.其他
1.动态函数调用
反射可用于调用未知类型的函数。例如,动态调用结构体的方法:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
}
func (p Person) Greet() {
fmt.Println("Hello,", p.Name)
}
func main() {
p := Person{Name: "Alice"}
v := reflect.ValueOf(p) // 获取 Person 的反射值
method := v.MethodByName("Greet") // 获取 Greet 方法
// 调用方法
method.Call(nil) // 输出: Hello, Alice
}
2.通用的打印函数
反射可用于实现一个通用的打印函数,支持任何类型:
package main
import (
"fmt"
"reflect"
)
func Print(value interface{}) {
v := reflect.ValueOf(value) // 获取值的反射
fmt.Printf("Type: %s\n", v.Type())
fmt.Printf("Value: %v\n", v)
}
func main() {
Print(42) // 输出 Type: int Value:42
Print("Hello") // 输出 Type: string Value:Hello
Print([]int{1, 2, 3}) // 输出 Type: []int Value:[1 2 3]
}
3.自定义比较函数
反射可用于比较两个未知类型的变量:
package main
import (
"fmt"
"reflect"
)
func AreEqual(a, b interface{}) bool {
return reflect.DeepEqual(a, b)
}
func main() {
x := 5
y := 5
fmt.Println(AreEqual(x, y)) // 输出: true
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println(AreEqual(s1, s2)) // 输出: true
}
4.动态序列化
反射可用于动态序列化和反序列化数据,例如自定义的 JSON 序列化:
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type User struct {
Name string
Email string
}
func JSONMarshal(v interface{}) ([]byte, error) {
value := reflect.ValueOf(v)
t := value.Type()
if t.Kind() != reflect.Map {
return nil, fmt.Errorf("expected a map, got %s", t.Kind())
}
return json.Marshal(v)
}
func main() {
user := map[string]string{
"Name": "Alice",
"Email": "alice@example.com",
}
data, err := JSONMarshal(user)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(data)) // 输出: {"Name":"Alice","Email":"alice@example.com"}
}
16. 结构体中Tag标签
1.基本用法
其实相当于注释,但是是可以被获取的,用``表示
package main
import (
"fmt"
"reflect"
)
type Time struct{
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex" doc:"性别"`
}
func FindTag(arg interface{}){
t := reflect.TypeOf(arg).Elem() // .Elem() 获取指针所指向的值的类型,即 Time 类型
// 如果不使用这个,则需要传值而不是传地址
for i := 0; i < t.NumField(); i++{
Tag_Info := t.Field(i).Tag.Get("info") //t.Field(i).Tag 获取字段的标签
Tag_Doc := t.Field(i).Tag.Get("doc")
//.Get("info") 和 .Get("doc") 分别获取 info 和 doc 标签的值
fmt.Println("info :",Tag_Info,"doc :",Tag_Doc)
}
}
func main(){
var re Time
FindTag(&re)
}
/*
info : name doc : 我的名字
info : sex doc : 性别
*/
2.在Json中的作用
包含头文件"encoding/json"
,看下面这个例子:
package main
import (
"encoding/json"
"fmt"
)
type Game struct{
Name string `json:"title"`
Year int `json:"year"`
Auth string `json:"author"`
}
func main(){
game := Game{"StarRailway",2023,"miHoYo"}
json_str , err := json.Marshal(game)
if err != nil{
fmt.Println("error .....")
return
}
fmt.Printf("jsonstr =%s\n",json_str)
my_Game := Game{}
err = json.Unmarshal(json_str, &my_Game)
if err != nil{
fmt.Println("error !")
}
fmt.Println("mygame =",my_Game)
}
/*
jsonstr ={"title":"StarRailway","year":2023,"author":"miHoYo"} //就变成json格式了
mygame = {StarRailway 2023 miHoYo}
*/
其中:
-
json.Marshal
将game
转换为 JSON 字符串:{"title":"StarRailway","year":2023,"author":"miHoYo"}
反序列化
my_Game := Game{}
err = json.Unmarshal(json_str, &my_Game)
json.Unmarshal
将 JSON 字符串json_str
解析为my_Game
:my_Game
的字段值与game
一致
JSON 标签的必要性:
- 如果没有
json
标签,JSON 字符串中的字段名将与结构体字段名一致(因 Go 字段名是大写的)
错误处理:
- 如果
json.Marshal
或json.Unmarshal
出现错误(例如字段类型不匹配),程序会退出
大写字段:
- 结构体字段必须是大写(导出字段),否则无法序列化
指针传递:
json.Unmarshal
需要传入指针才能修改值(&my_Game
)
补充:
如果在上述代码中你打印json_str
用的是Println,会出现一长串数字,是因为:
json_str
是通过 json.Marshal
生成的,它的类型是 []byte
,fmt.Println
打印 []byte
类型时会输出字节的数值,而不是可读的字符串
所以要正确打印就需要
fmt.Println(string(json_str)) // 打印完整的 JSON 字符串
fmt.Printf("%s",json_str)
3.其他
排除字段
如果希望某个字段不参与序列化或处理,可以将标签值设置为 -
。例如:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"-"` // 排除该字段
}
标签的格式
标签的值可以包含多个键值对,用逗号分隔。例如:
type User struct {
Name string `json:"name,omitempty"`
Email string `json:"email"`
}
omitempty
表示如果字段为空(零值),则在序列化时省略该字段
17. Goroutine
- 定义 :goroutine 是一种轻量级的线程,由 Go 运行时环境管理和调度,而不是直接由操作系统内核线程管理。它可以在一个独立的函数或方法上调运行,与主线程或其他 goroutine 并发执行
- 启动 :通过在函数调用前加上关键字
go
,就可以启动一个新的 goroutine - 调度与执行 :
- 调度机制 :Go 运行时通过一个称为 Goroutine Scheduler 的调度器来管理 goroutine 的执行。调度器会根据系统资源和程序运行情况,动态地将 goroutine 分配到少量的系统线程上运行,这些系统线程由 Go 运行时自动管理
- 执行模型 :当一个 goroutine 被启动后,它并不是立即执行,而是进入一个就绪队列,等待调度器分配给它运行的机会。调度器会根据一定的策略,如时间片轮转等,从就绪队列中选择 goroutine 进行执行
1.基本调用
package main
import(
"fmt"
"time"
)
func NewTask(){
i := 0
for{
i++
fmt.Println("new Goroutine: i =",i,"\n")
time.Sleep(1 * time.Second)
}
}
func main(){
//创建一个go进程去执行NewTask()
go NewTask()
i := 0
for{
i++
fmt.Println("main Goroutine: i =",i,"\n")
time.Sleep(1 * time.Second)
}
}
/*
main Goroutine: i = 1
new Goroutine: i = 1
new Goroutine: i = 2
main Goroutine: i = 2
main Goroutine: i = 3
new Goroutine: i = 3
new Goroutine: i = 4
main Goroutine: i = 4
...
*/
以上可以看出,goroutine和主进程是并发进行的
2.调用匿名函数
package main
import (
"fmt"
"time"
)
func main(){
go func(){
defer fmt.Println("A.defer")
func(){
defer fmt.Println("B.defer")
fmt.Println("B")
}() // 调用匿名函数需要在最后面加括号
fmt.Println("A")
}()
for{
time.Sleep(1 *time.Second)
}
}
/*
B
B.defer
A
A.defer
*/
想要结束一个goroutine可以使用runtime.Goexit()
使其在过程中退出,需要包含"runtime"
头文件
3.调用有形参函数
package main
import (
"fmt"
"time"
)
func main(){
go func(a int,b int) bool{
fmt.Println("a =",a,"b =",b)
return true
}(10,20) //后面括号跟传值,直接执行
for{
time.Sleep(1 *time.Second)
}
}
但是如果你想得到返回值,比如:
flag := go func(a int,b int) bool{
fmt.Println("a =",a,"b =",b)
return true
}(10,20) //后面括号跟传值,直接执行
//会直接报错
同样的,你换成:
flag := go func(a int,b int) int{
fmt.Println("a =",a,"b =",b)
return a*b
}(10,20)
也是一样的报错滴
那么怎么样能在goroutine中返回值呢?channel
√
18. Channel
Channel 是 Go 语言中的一个核心概念,用于在不同的 goroutine之间进行通信和数据交换
1.创建
使用make
函数创建 channel,语法为ch := make(chan Type)
,其中Type
是 channel 传递的数据类型
make(chan Type)
make(chan Typem, bufferSize)
//bufferSize表示缓冲区最多可以存储的数据元素个数
有缓冲 channel 允许在没有接收者的情况下发送一定数量的数据,只要缓冲区未满,发送操作就不会阻塞;同样,当缓冲区中有数据时,接收操作也不会阻塞
2.基本使用
channel <- value //发送value到channel
<- channel //接受并丢掉(因为没有变量接受)
x := <- channel //从channel中获取数据,并赋值给x
x, ok := <- channel //同上,并检查通道是否为空或者是否关闭
看个实例:
package main
import "fmt"
func main(){
c := make(chan int)
go func(){
defer fmt.Println("ending...")
fmt.Println("starting...")
c <- 555
}()
num := <- c
fmt.Println("num =",num)
fmt.Println("END")
}
/*
starting...
ending...
num = 555
END
*/
3.有缓冲和无缓冲问题
无缓冲:如上面那个实例,如果执行到num := <- c
这一步时但此时 c <- 555
还未发生(就是比主程序慢了一点),就会发生阻塞,使num赋值的那一步等待c的赋值和传入
(图源:上文视频,水印勿看)
实例:(此时循环超出给定容量)
package main
import (
"fmt"
"time"
)
func main(){
c := make(chan int, 3)
fmt.Println("len =",len(c),"cap =",cap(c))
go func(){
defer fmt.Println("ending...")
fmt.Println("starting...")
for i := 0;i < 6;i++{
c <- i
fmt.Println("发送与元素:",i,"len =",len(c),"cap =",cap(c))
}
}()
time.Sleep(2 *time.Second)
for i := 0;i < 6; i++{
num := <- c
fmt.Println("num = ",num)
}
fmt.Println("END")
}
/*
len = 0 cap = 3
starting...
发送与元素: 0 len = 1 cap = 3
发送与元素: 1 len = 2 cap = 3
发送与元素: 2 len = 3 cap = 3
num = 0
num = 1
num = 2
num = 3
发送与元素: 3 len = 3 cap = 3
发送与元素: 4 len = 0 cap = 3
发送与元素: 5 len = 1 cap = 3
ending...
num = 4
num = 5
END
*/
此时就发生了阻塞现象
有缓冲:
(图源:上文视频,水印勿看)
实例:(将赋值循环限制在规定容量中)
package main
import (
"fmt"
"time"
)
func main(){
c := make(chan int, 3)
fmt.Println("len =",len(c),"cap =",cap(c))
go func(){
defer fmt.Println("ending...")
fmt.Println("starting...")
for i := 0;i < 3;i++{
c <- i
fmt.Println("发送与元素:",i,"len =",len(c),"cap =",cap(c))
}
}()
time.Sleep(2 *time.Second)
for i := 0;i < 3; i++{
num := <- c
fmt.Println("num = ",num)
}
fmt.Println("END")
}
/*
len = 0 cap = 3
starting...
发送与元素: 0 len = 1 cap = 3
发送与元素: 1 len = 2 cap = 3
发送与元素: 2 len = 3 cap = 3
ending...
num = 0
num = 1
num = 2
END
*/
4.关闭channel
关闭 channel
- 关闭操作 :使用
close(ch)
可以关闭一个 channel,关闭后不能再向该 channel 发送数据,但仍然可以接收数据 - 接收已关闭 channel 的数据 :当从一个已关闭的 channel 接收数据时,如果 channel 中还有剩余数据,会继续接收这些数据;当数据接收完毕后,再次接收会立即返回该类型的零值
实例:
package main
import "fmt"
func main(){
c := make(chan int)
go func(){
for i := 0; i < 5; i++{
c <- i
}
close(c)
}()
for{
if data, ok := <-c; ok{
fmt.Println(data)
}else{
break
}
}
fmt.Println("ENDING")
}
/*
0
1
2
3
4
ENDING
*/
这里我们看到关闭了通道后正常输出,如果没关闭呢:
在这里如果没有关闭channel的话,主程序中的for循环的ok一直会返回true,但此时c已经没有数据了,关闭之后,ok判断为false,则结束循环
注意:
- 但确实没有发送任何数据了,或者想显式的结束range循环,再关闭channel
- 关闭后无法向channel发送任何数据
- 关闭之后可以继续从channel中接收数据
- 对于nil channel,无论收发都会被阻塞
对应注意2,如果:
go func(){
for i := 0; i < 5; i++{
c <- i
close(c)
}
}()
/*
panic: send on closed channel
goroutine 19 [running]:
main.main.func1()
*/
对应注意4:
var ch chan int //这就是个nil channel,未被初始化
ch <- 1 // 阻塞,直到 channel 被初始化
<- ch // 阻塞,直到 channel 被初始化
close(ch) // 运行时错误:panic: close of nil channel
5.channel & range
实例:
for data := range c{
fmt.Println(data)
}
range的作用是如果c中一旦有数据就传给data,没有的话就阻塞
可以使用range来迭代不断操作channel
6.channel & select
- 多路复用 :
select
可以同时监听多个 channel 上的事件,当任何一个 channel 上的事件发生时,执行相应的代码块。这使得一个 goroutine 可以同时处理多个 channel 上的通信 - 阻塞和非阻塞 :如果没有
default
分支,select
会阻塞,直到其中一个 channel 操作可以执行。如果有default
分支,select
不会阻塞,而是立即执行default
分支 - 随机选择 :如果有多个 channel 操作同时可以执行,
select
会随机选择其中一个执行,其他操作会被忽略。这种随机性可以用于实现公平性或避免死锁
select可以完成监控多个channel的状态:
select{
case <- chan1:
//如果chan1读到数据就执行这句话
case <- chan2:
default:
//如果上面都没有成功,则执行default语句
}
1.实例:(超时处理)
ch := make(chan int)
timeout := time.After(2 * time.Second)
select {
case <-ch:
fmt.Println("Received data from channel")
case <-timeout:
fmt.Println("Timeout occurred")
}
//如果在指定时间内没有收到数据,执行超时处理逻辑
2.实例:(监听多个)
//原视频的斐波那契数列算法错了,以下应该是求二倍
package main
import (
"fmt"
)
func Calculate(c, quit chan int){
x, y := 1 , 1
for{
select{
case c<-x:
x = y
y += x
case <- quit:
fmt.Println("quit...")
return
}
}
}
func main(){
c := make(chan int)
quit := make(chan int)
go func(){
for i := 0; i<6; i++{
fmt.Println(<-c)
}
quit <- 0 //执行完程序就就赋值quit,触发第二个case
}()
Calculate(c,quit)
}
3.实例:(关闭channel)
ch := make(chan int)
close(ch)
select {
case v, ok := <-ch:
fmt.Println("Received from closed channel:", v, "OK:", ok)
}
当一个 channel 被关闭后,select
仍然可以选择该 channel 的接收操作,但会立即返回该类型的零值
19. GoModuels
Go Modules 是 Go 语言官方推出的一种包管理机制,旨在解决 Go 语言项目中的依赖版本管理问题。自 Go 1.11 版本开始引入,Go 1.13 版本成为默认的依赖管理方式,并逐渐取代早期的 GOPATH
模式
1.为什么使用 Go Modules
- 版本控制:可以为依赖库指定特定的版本,避免库的版本更新导致项目不可用
- 离线工作:Go Modules 在本地缓存依赖项,允许在没有网络连接时继续开发
- 模块隔离:每个项目都可以独立管理其依赖项,不再依赖全局的
GOPATH
- 可重现构建:每次构建都可以使用相同的依赖版本,保证项目的一致性
2.go mod 命令
1.初始化模块
终端使用 go mod init
命令初始化 Go Modules 文件(go.mod
),它会在项目根目录生成一个 go.mod
文件,记录模块名和 Go 版本等信息
go mod init 你的文件名
2.添加依赖
在终端使用(下面同理) go get
命令下载并安装依赖包,它会自动将依赖项的版本记录到 go.mod
中
go get github.com/gin-gonic/gin
如果需要安装特定版本的依赖包,可以在包名后面加上 @<version>
:
go get github.com/gin-gonic/gin@v1.7.2
3.更新依赖
使用 go get -u
命令更新依赖项到最新的次版本或修订版本
go get -u github.com/gin-gonic/gin
4.清理依赖
使用 go mod tidy
命令清理不再使用的依赖项,并确保 go.mod
和 go.sum
文件是最新的。
go mod tidy
5.列出依赖项
使用 go list -m all
命令可以列出所有依赖项的模块及其版本。
go list -m all
6.部分命令
命令 | 介绍 |
---|---|
go mod init | 初始化项目依赖,生成 go.mod 文件 |
go mod download | 根据 go.mod 文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与 go.mod 进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑 go.mod 文件 |
go mod vendor | 将项目的所有依赖导出至 vendor 目录(可用于无网络条件) |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
3.go mod环境变量
1.常见的 Go Modules 环境变量
环境变量 | 描述 |
---|---|
GO111MODULE | 是否开启Go Modules模式 |
GOPROXY | 项目第三方依赖库的下载地址(建议设置国内的地址) |
GOSUMDB | 用来检验拉取的第三方库是否完整 |
GOMODCACHE | 设置 Go 模块缓存目录的路径 |
2.介绍
1. GO111MODULE
-
作用:控制 Go Modules 的启用状态
-
取值:
on
:启用 Go Modules,即使在GOPATH
模式下也会使用 Go Modulesoff
:禁用 Go Modules,使用GOPATH
模式auto
(默认值):在项目根目录有go.mod
文件时启用 Go Modules,否则使用GOPATH
模式
-
示例:
# 启用 Go Modules export GO111MODULE=on # 禁用 Go Modules export GO111MODULE=off # 使用默认值 export GO111MODULE=auto
2. GOPROXY
-
作用:设置 Go 模块代理的地址
-
取值:一个或多个以逗号分隔的代理地址。支持的协议包括
https
、http
和file
-
示例:
export GOPROXY=https://mirrors.aliyun.com/goproxy/ #阿里云 export GOPROXY=https://goproxy.cn,direct #七牛云 #direct用于指示GO回源到模块版本的源地址去抓取(GitHub....)
3. GOSUMDB
-
作用:设置校验和数据库的地址
-
取值:一个或多个以逗号分隔的校验和数据库地址。支持的协议包括
https
、http
和file
-
示例:
export GOSUMDB=sum.golang.org
4. GOMODCACHE
-
作用:设置 Go 模块缓存目录的路径
-
取值:一个有效的文件系统路径
-
示例:
export GOMODCACHE=/path/to/cache
3.常用命令总结
命令 | 介绍 |
---|---|
go env | 显示当前 Go 环境变量的值 |
go env -w | 设置环境变量的值 |
go env -u | 删除环境变量的值 |