MyGo(速通Golang基础)

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是创建新的类型,并非只提供别名)

  1. 定义自定义类型:可以基于内置类型或结构体来定义新的类型,使其具有特定的特性
  2. 定义结构体:用 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)。反射允许你在运行时获取和操作这两个部分

  • 类型:表示变量的数据类型,例如 intstringstruct
  • :表示变量的实际数据内容,例如字符串 Hello 或整数 42

Go 的反射机制主要依赖于以下两个核心方法:

  1. reflect.TypeOf(v interface{}):返回变量 v 的类型
  2. 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.Marshalgame 转换为 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.Marshaljson.Unmarshal 出现错误(例如字段类型不匹配),程序会退出

大写字段

  • 结构体字段必须是大写(导出字段),否则无法序列化

指针传递

  • json.Unmarshal 需要传入指针才能修改值(&my_Game
补充:

如果在上述代码中你打印json_str用的是Println,会出现一长串数字,是因为:

json_str 是通过 json.Marshal 生成的,它的类型是 []bytefmt.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,则结束循环

注意:

  1. 但确实没有发送任何数据了,或者想显式的结束range循环,再关闭channel
  2. 关闭后无法向channel发送任何数据
  3. 关闭之后可以继续从channel中接收数据
  4. 对于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.modgo.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 Modules
    • off:禁用 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 模块代理的地址

  • 取值:一个或多个以逗号分隔的代理地址。支持的协议包括 httpshttpfile

  • 示例

    export GOPROXY=https://mirrors.aliyun.com/goproxy/  #阿里云
    export GOPROXY=https://goproxy.cn,direct #七牛云
    
    #direct用于指示GO回源到模块版本的源地址去抓取(GitHub....)
    
3. GOSUMDB
  • 作用:设置校验和数据库的地址

  • 取值:一个或多个以逗号分隔的校验和数据库地址。支持的协议包括 httpshttpfile

  • 示例

    export GOSUMDB=sum.golang.org
    
4. GOMODCACHE
  • 作用:设置 Go 模块缓存目录的路径

  • 取值:一个有效的文件系统路径

  • 示例

    export GOMODCACHE=/path/to/cache
    
3.常用命令总结
命令介绍
go env显示当前 Go 环境变量的值
go env -w设置环境变量的值
go env -u删除环境变量的值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值