go基础第二遍学习——简单易理解

news2024/11/18 11:46:01

此博文是猿猿对go基础的二遍学习,这一遍学习中对go基础有了进一步的理解,笔记齐全,下面跟着猿猿一起学习吧。


文章目录

      • 零、知识补充
      • 一、包引入三种方式
      • 二、go变量和基本数据类型
        • 2.1.基本数据类型
          • 2.1.1.整数类型
          • 2.1.2.浮点类型
          • 2.1.3.字符串类型
          • 2.1.4.布尔类型
          • 2.1.5.类型判断
          • 2.1.6.数据类型转化
            • string到int
            • string到int64
            • int到string
            • int64到string
            • 字符串到float32/float64
            • int64转int
            • int转int64
        • 2.2.复杂数据类型
      • 三、流程控制语句
          • 1、控制整数自身的增加和减少
          • 2、条件语句
          • 3、选择语句
          • 4、循环语句
          • 5、跳转语句
      • 四、数组和切片
        • 1、数组
          • 1.1、定义数组
          • 1.2、自动确定长度
          • 1.3、循环数组
          • 1.4、多维数组
        • 2、切片
          • 2.1、 创建切片的方式
          • 2.2、append
          • 2.3、copy
          • 2.4、快速创建切片
      • 五、map的声明和使用
        • 1、介绍
        • 2、三种声明方式
        • 3、map的使用
        • 4、删除key
        • 5、遍历
      • 六、func 函数方法
        • 1、声明
          • 1.1、基本结构
          • 1.2、匿名函数
          • 1.3、不行参函数
        • 2、特殊函数
          • 2.1、自执行函数
          • 2.2、闭包函数
          • 2.3、延迟调用函数
      • 七、指针和地址
        • 1、指针、地址
        • 2、数组指针和指针数组
        • 3、函数指针
          • 3.1、指针函数
          • 3.2、非指针函数
        • 4、隐式指针
      • 八、结构体struct
        • 1、含义
          • 声明
        • 2、作为数据类型的三种方式
            • 方式一:
            • 方式二:
            • 方式三:
        • 3、访问参数
        • 4、方法参数
        • 5、结构体指针
        • 6、结构体嵌套
      • 九、接口 interface
        • 1、定义
        • 2、使用
        • 3、具体工作里面的用法
      • 十、并发 (goroutine、channel)
        • 1、什么是goroutine
          • 1.1、示例
          • 1.2、协程管理
        • 2、什么是channel
          • 2.1、channel分为五种
          • 2.2、**channel是可以关闭的**
          • 2.3、select的用法:
          • 2.4、channel和goroutine交互
      • 十一、断言和反射Assertion、reflect
        • 1、断言
        • 2、反射
          • 2.1、为什么要用反射
          • 2.2、反射的用法和主要函数
      • 十二、sync包的使用
        • 1、Mutex 互斥锁
        • 2、RWMutex 读写互斥锁
        • 3、Once
        • 4、WaitGroup
        • 5、Map 一个并发字典
        • 6、Pool 并发池
        • 7、Cond 没多大用的通知解锁
      • 十三、golang 基础之 文件操作
        • 1、io包基础接口
          • 1.1、Reader
          • 1.2、Writer
          • 1.3、Seeker
          • 1.4、Closer
        • 2、一些常量
        • 3、读取文件的几个关键方法
        • 4、写文件的几个关键方法
        • 5、复制文件
      • 十四、net 包
        • 1、tcp
          • 1.1、客户端
          • 1.2、服务端
        • 2、http
          • 2.0、重要的类型和接口
          • 2.1、服务端
          • 2.2、客户端
        • 3、rpc ***
          • 3.1、注意事项
          • 3.2、服务端
          • 3.3、客户端
      • 十五、泛型
        • 1、泛型基本含义
        • 2、泛型方法
        • 3、泛型结构体
        • 4、泛型map
        • 5、泛型切片
        • 6、创建泛型约束
        • 7、~的含义
        • 8、泛型结构体的泛型方法
        • 9、总结
          • 9.1、人家提供的泛型约束
          • 9.2、定义自己的泛型约束
      • 十六、websocket
        • 1.创建websocket服务端
        • 2、创建websocket客户端

golang 1010工作室

go基础第一遍学习:https://blog.csdn.net/qq_52395816/article/details/125330105?spm=1001.2014.3001.5502
视频地址:【golang教学】第一章:golang的安装和编辑工具安装(1010工作室出品)_哔哩哔哩_bilibili

零、知识补充

%v %+v %#v的区别

package main

import "fmt"

type student struct {
    name string
    id int
}

func main() {
    s := &student{"jiafu", 123456}
    fmt.Printf("%%v的方式  = %v\n", s)
    fmt.Printf("%%+v的方式 = %+v\n", s)
    fmt.Printf("%%#v的方式 = %#v\n", s)
}

输出结果如下:

%v的方式  = &{jiafu 123456}
%+v的方式 = &{name:jiafu id:123456}
%#v的方式 = &main.student{name:"jiafu", id:123456}

总结

  1. %v 只输出所有的值
  2. %+v 先输出字段类型,再输出该字段的值
  3. %#v 先输出结构体名字值,再输出结构体(字段类型+字段的值)

一、包引入三种方式

  • 直接引入
  • 别名引入
  • . 引入

testpackage.go

  • 包里面的方法名或者是变量名是大写开头,才能拿给别的包使用;如果是小写字母开头,变量私有
  • 一个文件夹下有一个包,但是可以有多个文件
package testpackage

var A string = "包测试 1010"

main.go

package main

import (
   "fmt"
   "goclass/testpackage"
   . "goclass/testpackage"
   coll "goclass/testpackage"
)

func main() {
   var a string = "hello 2020"
   //关键字  变量名  变量类型  =  变量值
   b := "hello 1010"
   //隐式声明

   fmt.Println(a)
   fmt.Println(b)
   fmt.Println(testpackage.A)
   fmt.Println(coll.A)
   fmt.Println(A)
}

请添加图片描述

二、go变量和基本数据类型

百度脑图-便捷的思维工具 (baidu.com)

2.1.基本数据类型

2.1.1.整数类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6Fek71K-1670809517365)(golang 1010工作室.assets/image-20221202090556090.png)]

2.1.2.浮点类型
  • float32
  • float64(一般使用这个)
package main

import "fmt"

func main() {
   var num1 float64
   num1 = 3.141492654
   fmt.Println(num1)
}
2.1.3.字符串类型
2.1.4.布尔类型
2.1.5.类型判断
var num1 float64
num1 = 3.141492654
fmt.Printf("%T", num1)
//输出 float64
2.1.6.数据类型转化
string到int
int,err := strconv.Atoi(string)
string1 := "123"
int111, _ := strconv.Atoi(string1)
fmt.Println(int111)
fmt.Printf("%T", int111)
string到int64
int64, err := strconv.ParseInt(string1, 10, 64)
int到string
string := strconv.Itoa(int)
int64到string
string := strconv.FormatInt(int64, 10)
字符串到float32/float64
float32,err := ParseFloat(string,32)

float64,err := ParseFloat(string,64)
int64转int
int := int(int64)
int转int64
int64 := int64(int)

2.2.复杂数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2HAoeRDI-1670809002567)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209161940829.png)]

三、流程控制语句

1、控制整数自身的增加和减少
2、条件语句
if(a==1){
}elseif(a==2){
}else{
}
3、选择语句
a := 1
switch a {
case 0:
fmt.Println("a=0")
fallthrough
case 1:
fmt.Println("a=1")
default:
fmt.Println("都不是")
}

fallthroughGo里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fall****through强制执行后面的case代码

4、循环语句
for{
	a++
	if a>3{
		break
	}
}
for a:=0;a<3;a++{
	fmt.Println(a)
}
for a<3{
	a++
	fmt.PrintLn(a)
}
5、跳转语句

break 与 continue 配合标签可用于多层循环嵌套的跳出

goto 是调整执行位置,与其他2个语句配合标签的结果并不相同

	//定义循环体A
A:
	for {
		for i := 0; i < 10; i++ {
			fmt.Println(i)
			if i > 3 {
				//跳出A
				break A
				//跳到B
				goto B
			}
		}
	}
	fmt.Println("结束")

	//定义循环体B
B:
	fmt.Println("我在B")
A:
   for a := 0; a < 10; a++ {
      //a++
      fmt.Println(a)
      if a == 4 {
         break A
         goto B
      }
   }
   fmt.Println("结束")
B:
   fmt.Println("我在B")
}
输出:
0
1    
2    
3    
4    
结束 
我在B

四、数组和切片

1、数组

1.1、定义数组

定义:数组内元素类型[数组长度]

[元素长度]元素类型{元素1,元素2,......}
a:= [3]int{1,2,3}
//先new一个数组,在赋值
p := new ([10]int)
new([数组长度]元素类型)

p[2] = 5
1.2、自动确定长度
b := [...]int{1, 2, 3, 5, 6, 4, 5}
1.3、循环数组
// 根据数组长度进行for循环 取数组的当前下标位置的数据
	zoom := [...]string{"猴子", "猫", "狗子"}
	for i := 0; i < len(zoom); i++ {
		fmt.Println(zoom[i] + "跑")
	}
// 自动遍历所有元素 i为下标 v为当前元素
	for i, v := range zoom {
		fmt.Println(i, v)
	}
	fmt.Println(a, b, c)

数组长度len(),数组容量cap()

  • len() 取数组或者切片长度
  • cap()取数组或者切片容量
  • 在数组中 他们两个永远相同
fmt.Println(len(zoom), cap(zoom))
输出:
猴子跑
猫跑                                          
狗子跑                                        
0 猴子                                        
1 猫                                          
2 狗子  
3 3  
1.4、多维数组
	d := [2][3]int{
		{0, 1, 2},
		{5, 6, 8},
	}

2、切片

  • 切片 是数组的一部分;切片不是数组,它指向数组底层,会改变原数组

  • 切片可以越界,数组不可以越界

  • 使用append后在改变切片某个元素,不会改变原数组

  • 向切片里添加元素,可以扩充cap

  • 当len长度超过cap的时候 会自动分配内存

  • 切片的len是当前切片中元素的个数
    切片的cap是他取自的数组的cap

2.1、 创建切片的方式
//先创建一个数组
a := [10]int{1,2,3,4,5,6,7,8,9}
fmt.Println(a)

//取一个值 此时已经是切片
sl := a[5]

//截取索引5 到 索引9,不包含 10  此时已经是切片
sl := a[5:10]

//从 索引为3处 截取到尾部  此时已经是切片
sl := a[3:]

//从头取到索索引为 5  此时已经是切片
sl := a[:5]

//完全拷贝  此时已经是切片
sl := a[:]

//make创建 与len和cap相同时的简写

切片名称:=make([]元素类型,len,cap)
sl := make([]int,3,3)
sl := make([]int,3,)
sl := make([]int,3)

fmt.Println(sl)
2.2、append

切片 是数组的一部分

	a := [3]int{0, 1, 2}
	cl := a[2:]
	fmt.Println(cl)
	cl[0] = 5
	fmt.Println(cl)
	fmt.Println(a)
	//输出
	//[2]
	//[5]
	//[0 1 5]

切片可以越界,数组不可以越界

使用append后在改变切片某个元素,不会改变原数组

a := [3]int{0, 1, 2}
cl := a[1:] //全拷贝
fmt.Println(cl)
cl = append(cl, 5)  //使用append后在改变切片某个元素,不会改变原数组
cl[0] = 4
fmt.Println(cl)

//输出
//[1 2]
//[4 2 5]

切片容量和长度

	a := [3]int{0, 1, 2}
	cl := a[:]
	fmt.Println(cl)  //0,1,2
	fmt.Println(len(cl), cap(cl))  //3  3
	cl = append(cl, 5)
	fmt.Println(cl)   //0,1,2,5
	fmt.Println(len(cl), cap(cl))  //4  6
	//输出
	//[0 1 2]
	//3 3
	//[0 1 2 5]
	//4 6
	a := [3]int{0, 1, 2}
	cl := a[:]
	fmt.Println(cl)
	fmt.Println(len(cl), cap(cl))
	cl = append(cl, 5)
	cl = append(cl, 5)
	cl = append(cl, 5)
	cl = append(cl, 5)
	fmt.Println(cl)
	fmt.Println(len(cl), cap(cl))

	//输出
	//[0 1 2]
	//3 3
	//[0 1 2 5 5 5 5]
	//7 12
2.3、copy
	a := [3]int{0, 1, 2}
	cl := a[:]
	cl1 := a[2:]
	cl = append(cl, 5)
	copy(cl, cl1)
	fmt.Println(cl)
	//结果为	[2 1 2 5]

	//把第二个切片里的东西拷贝到第一个切片里覆盖
	d := []int{1, 2, 3, 4, 5, 6}
	b := []int{7, 8, 9}
	copy(d, b)
	//结果为 [7 8 9 4 5 6]
	fmt.Println(d)

	copy(d[2:4], b[0:2])
	//结果为 [1 2 7 8 5 6]
	fmt.Println(d)
2.4、快速创建切片
	//快速创建切片
	var aa []int
	aa = append(aa, 123)

	aaa := make([]int, 4, 4)
	
	fmt.Println(aa)
	fmt.Println(aaa)
	
	//输出
	//[123]
	//[0 0 0 0]

五、map的声明和使用

1、介绍

map是类似于其他语言字典或者哈希表的东西表现为key-value

2、三种声明方式

//map三种声明方式
//1.var
var m map[string]string
m = map[string]string{}
m["name"] = "qm"
m["age"] = "30"
fmt.Println(m)


//可以直接使用的map
//2.隐式声明
m1 := map[string]string{}
m1["name"] = "qm"
m1["age"] = "30"
fmt.Println(m1)

//3.make
m2 := make(map[string]string)
m2["name"] = "qm"
m2["age"] = "30"
fmt.Println(m2)

3、map的使用

m1 := map[int]bool{}
m1[2] = true
m1[1] = false
fmt.Println(m1)

map想要传任何值怎么办 ------> 使用空接口作为值

	m2 := map[int]interface{}{}
	m2[1] = false
	m2[2] = "小明"
	m2[3] = []int{1, 2, 5}
	fmt.Println(m2)

4、删除key

delete(变量名,key)  

5、遍历

	for k, v := range m2 {
		fmt.Println(k, v)
	}

m2 := map[string]interface{}{}
m2["a"] = false
m2["b"] = "小明"
m2["c"] = []int{1, 2, 5}
fmt.Println(m2, len(m2))

delete(m2, "c")
fmt.Println(m2, len(m2))

for k, v := range m2 {
   fmt.Println(k, v)
}
	//输出
	//map[a:false b:小明 c:[1 2 5]] 3
	//map[a:false b:小明] 2
	//a false
	//b 小明

六、func 函数方法

1、声明

1.1、基本结构
func 函数名 (入参1 入参类型,入参2 入参类型...)(出参1 出参类型,出参2 出参类型){
		函数体 // 写一些逻辑代码  一定要return你的出参 符合出参类型 且入参一定要使用
}

实例

fun main () {
	//那边有出参,这边接收参数
	r1, r2 := a(0, " data2的数据")
	fmt.Println(r1, r2)
}


//		入参						出参
func a(data1 int, data2 string) (ret1 int, ret2 string) {
	if data1 > 1 {
		return data1, "现在是大于1的情况,并且data2为" + data2
	} else {
		return data1, "现在是小于1的情况,并且data2为" + data2
	}
}

//输出:  0 现在是小于1的情况,并且data2为 data2的数据
func a(data1 int, data2 string) (ret1 int, ret2 string) {
	//省略声明,省略返回值的写法
	ret1 = data1
	ret2 = data2
	return
}
1.2、匿名函数
A:= func(){}

//函数不能写在函数内部,匿名函数可以(本质还是在外面声明的)

func main() {

	b := func(data string) {
		fmt.Println(data)
	}
	b("我是匿名函数")
}

本质

func bb(data string) {
		fmt.Println(data)
}
func main() {
	b := bb
	b("我是匿名函数")
}
1.3、不行参函数

不定向传参是个切片,可进行切片相关操作

func UN (入参1 参数类型,入参2 ...参数类型 )(){
    fmt.Pringln(入参1,入参2)
	我们可以对入参2进行 for range
	不定参必须作为最后一项
}

实例

func main() {
	mo(9527, "1", "2", "3", "4")
	//输出
	//9527 [1 2 3 4]
	//0 1
	//1 2
	//2 3
	//3 4
	//等效于下面。你犹豫不决,我也犹豫不决
	ar := []string{"1", "2", "3", "4"}
	mo(9527, ar...)
}

//不定向参数
func mo(data1 int, data2 ...string) {
   fmt.Println(data1, data2)
   //data2是个切片,可进行切片相关操作
   for k, v := range data2 {
      fmt.Println(k, v)
   }
}

2、特殊函数

2.1、自执行函数
(func(){})()

实例

	//自执行函数
	(func() {
		fmt.Println("我在这里执行,别人别管我")
	})()
2.2、闭包函数

函数返回一个函数,就叫闭包函数。返回函数外面什么参数,里面就要对应什么参数

func()(func(A)){}
func moo() func(num int) {
	return func(num int) {
		
	}
}

闭包函数调用

func main() {
	moo()(4)
}

//闭包函数
func moo() func(num int) {
	return func(num int) {
		fmt.Println("闭包函数调用", num)
	}
}
2.3、延迟调用函数
defer

实例

func main() {
	defer mooo()
	fmt.Println("1")
	fmt.Println("2")
	fmt.Println("3")
	//输出
	//1
	//2
	//3
	//我想最先执行
}

func mooo() {
	fmt.Println("我想最先执行")
}

七、指针和地址

1、指针、地址

  • 用*表示 表示我们这个变量本身没有任何存储值而是拿到了原本数据的存储路径
  • 用 & 取地址,用 * 声明指针
var a int
a = 123
fmt.Println(a) //123

//b是值的时候,令b=a在改变b值,a不变
var b int
b = a
b = 456
fmt.Println(a, b) //123 456

//b指向a的内存地址的时候,在取b的指针 实际取的就是a
var bb *int
bb = &a            //把bb指向a的内存地址,现在a和bb是一条绳上的蚂蚱
*bb = 999          //把b的指针=999.  bb是a的地址,因此bb的指针就是a
fmt.Println(a, bb, *bb) //99 a的内存地址  999(拿出bb,找到地址,通过指针拿出来)
fmt.Println(a == *bb, &a == bb) //true  true(a的地址就是bb)

2、数组指针和指针数组

指针取到原始位置,并进行修改 ---->更改原始数据

//数组指针
var arr [5]string
arr = [5]string{"1,2,3,4,5"}
var arrP *[5]string
arrP = &arr
fmt.Println(arr, arrP)

//指针数组
var arrPP [5]*string
var str1 string = "str1"
var str2 string = "str2"
var str3 string = "str3"
var str4 string = "str4"
var str5 string = "str5"

//用指针,取原始地址,对原始地址的指针进行修改,原始地址也会变
arrPP = [5]*string{&str1, &str2, &str3, &str4, &str5}
*arrPP[2] = "555"
fmt.Println(str3)

	//输出
	//[1,2,3,4,5    ] &[1,2,3,4,5    ]
	//555
//指针数组
//var arrPP [5]*string
var arrPP [5]string
var str1 string = "str1"
var str2 string = "str2"
var str3 string = "str3"
var str4 string = "str4"
var str5 string = "str5"

//用指针,取原始地址,对原始地址的指针进行修改,原始地址也会变
//arrPP = [5]*string{&str1, &str2, &str3, &str4, &str5}
//*arrPP[2] = "555"
//fmt.Println(str3)

arrPP = [5] string {str1, str2, str3, str4, str5}
arrPP[2] = "555"
fmt.Println(str3)

//输出  str3

3、函数指针

3.1、指针函数
func main() {
	//指针传参
	str = "我定义了"
	pointFun(&str)
	fmt.Println(str)  //我变了
}

func pointFun(p1 &string) {
	*p1 = "我变了"
}
3.2、非指针函数

不会变,因为我们传的是值,而不是原始的内容

func main() {
	str = "我定义了"
	pointFun(str)
	fmt.Println(str)  //我定义了
}

func pointFun(p1 string) {
	p1 = "我变了"
}

4、隐式指针

var str1 string = "我是来测地址的"

p := &str1
*p = "我是来测地址的123"

fmt.Println(str1)   //我是来测地址的123

八、结构体struct

1、含义

  • 结构体是一个可以存储不同数据类型的数据类型
  • 因为再声明完以后 它可以和数据类型一样使用 也有指针 值 等等
  • 及其像json 所以交互用它没错
声明
type 结构体名 struct {
其他数据类型名 数据类型 `标签`

...
}
//结构体
type QiMiao struct {
   Name  string
   Age   int
   Sex   bool
   Hobby []string
}

2、作为数据类型的三种方式

方式一:
var qm Qm
//跟数据类型的使用方法一样
var qm QiMiao
qm.Name = "奇妙"
qm.Age = 18
qm.Hobby = []string{"吃饭", "睡觉", "写作业"}
qm.Sex = true

fmt.Println(qm)
方式二:
qm := QM{}
qm := QiMiao{
   Name:  "你好",
   Age:   0,
   Sex:   false,
   Hobby: []string{"你好","你好二号","你好三号","你好四号"},
}
fmt.Println(qm)
qm := QiMiao{"nihao", 18, true, []string{"你好", "你好二号", "你好三号", "你好四号"}}
fmt.Println(qm)
方式三:

new 会直接得到这个结构体的地址,其他方式声明会得到结构体本身

qm := new(QM)
//new 会直接得到这个结构体的地址,其他方式声明会得到结构体本身 
qm := new(QiMiao)
qm.Age = 18
qm.Sex = true
qm.Name = "nihao"
qm.Hobby = []string{"你好", "你好二号", "你好三号", "你好四号"}
fmt.Println(qm) //&{nihao 18 true [你好 你好二号 你好三号 你好四号]}

3、访问参数

qm.XXX

4、方法参数

func A(QM){}

%v 只输出所有的值

//结构体
type QiMiao struct {
   Name  string
   Age   int
   Sex   bool
   Hobby []string
}

//结构体方法传参
//先把结构体作为参数写在前面
func (qm *QiMiao) Song(name string) (restr string) {
   restr = "真是给了我一记惊雷啊"
   fmt.Printf("%v唱了一首%v,观众觉得%v", qm.Name, name, restr)
   return restr
}

func main() {
	qm := QiMiao{
		Name:  "唱歌的奇妙",
		Age:   0,
		Sex:   false,
		Hobby: nil,
	}

	res := qm.Song("惊雷")
	fmt.Println("\n", res)
}
	//输出
	//唱歌的奇妙唱了一首惊雷,观众觉得真是给了我一记惊雷啊
	//真是给了我一记惊雷啊

5、结构体指针

var qp  *QM

6、结构体嵌套

//结构体
type QiMiao struct {
   Name  string
   Age   int
   Sex   bool
   Hobby []string
   Myhome Home
}
type Home struct {
   P string
}
//给Home结构体挂载一个方法
func (h *Home) Open() {
	fmt.Println("open", h.P)
}

func main() {
	qm.Myhome.P = " 北京"
	fmt.Println(qm)
    qm.Myhome.Open()
    //	{唱歌的奇妙 0 false [] { 北京}}
    //	open  北京
}

九、接口 interface

1、定义

接口是一类规范 是某一些方法的集合

type Animal interfance{
   Run()
   Eat()
}

接口: 一类方法的集合,只要我实现了这些方法,我就属于这个接口

//接口     一类方法的集合,只要我实现了这些方法,我就属于这个接口
type Animal interface {
   Eat()
   Run()
}

type Cat struct {
   Name string
   Sex  bool
}

type Dog struct {
   Name string
}

func (c Cat) Eat() {
   fmt.Println(c.Name, "开始吃")
}

func (c Cat) Run() {
   fmt.Println(c.Name, "开始跑")
}

func (d Dog) Run() {
   fmt.Println(d.Name, "开始跑")
}

func (d Dog) Eat() {
   fmt.Println(d.Name, "开始吃")
}

2、使用

func main() {
//只要实现了这个接口的结构体,都可以用这个接口来接收
   var a Animal

   a = Cat{
      Name: "Tom",
      Sex:  false,
   }
	//用接口调用到了原始实例的方法
   a.Run()
   a.Eat()
}

3、具体工作里面的用法

  1. go没有什么好的实现泛型的方法 此时的接口可以用来实现泛型
type Animal interface {
	Eat()
	Run()
}

type Cat struct {
	Name string
	Sex  bool
}

func (c Cat) Eat() {
	fmt.Println(c.Name, "开始吃")
}

func (c Cat) Run() {
	fmt.Println(c.Name, "开始跑")
}

func main() {

	myFunction([]string{"123", "123"})  //[123 123]

	a := Cat{
		Name: "Tom",
		Sex:  false,
	}
	myFun(a)
	//Tom 开始跑
	//Tom 开始吃
}

//go没有什么好的实现泛型的方法 此时的接口可以用来实现泛型.
//空接口--没有方法,所有的类型都会实现空接口
//于是myFunction就可以接收任何的参数
func myFunction(a interface{}) {
   fmt.Println(a)
}

//我不管你原本的结构体是什么,你进来就得吃和跑
func myFun(a Animal) {
   //只要你符合动物结构体的条件,你就能使用这两个方法
   a.Run()
   a.Eat()
}
  1. 解耦合 比较类似java的一些方式 先声明一个接口 然后 一切方法都用这个接口来操作

    让代码更加规范,让团队协作更加方便的一个技术

//L是声明好放在这里的一个接口
var L Animal

func MyFun(a Animal) {
   L = a
}

func main() {

   //接口用户解耦合
   //我有一个全局变量L,我不知道它是什么结构体,但是我想让它什么时候都能进来Run和Eat
   var c = Cat{
      Name: "Tom",
      Sex:  false,
   }
   //很多地方要调用这个方法就要实现这个接口,不实现这个接口这个接口的方法就不能用。(因为接口规范在这里)
   MyFun(c) //调用这个方法会自动将这个接口进行挂在,挂在完之后在Run
   L.Run()
}

十、并发 (goroutine、channel)

1、什么是goroutine

在调用一个方法的前面加上 go就是 goroutine。 它会让方法异步执行 相当于协程

1.1、示例
func Run(){
	fmt.Println("123")
}

func main() {
	go Run()   //不会输出,因为它不会阻碍主程序执行了,这条线跑到了旁边,发现主程序什么也没干
}
//goroutine
func Run() {
   fmt.Println("我跑起来了")
}

func main() {

   go Run()  //和主程序并行执行,不影响主程序的执行
   //time.Sleep(1 * time.Second)
   i := 0
   for i < 500 {
      i++
      fmt.Println(i)  //224后go Run才会执行
   }
   
}
1.2、协程管理
var wg sync.WaitGroup

wg.Add()

wg.Done()

wg.Wait()

实例

//goroutine
func Run(wg *sync.WaitGroup) {
   fmt.Println("我跑起来了")
   wg.Done() //关闭
}

func main() {
   var wg sync.WaitGroup //声明协程管理器
   wg.Add(1)             //告诉它我有一个协程
   //在run中把协程结束----把协程传进去
   go Run(&wg) //和主程序并行执行,不影响主程序的
   wg.Wait()   //wg继续等待
}

2、什么是channel

channal是goroutine之间的通讯桥梁

2.1、channel分为五种

1.有缓冲区

c1 := make(chan int,5)

2.无缓冲区

c2 := make(chan int)

存和取

//定义有一个缓冲区的channel
c1 := make(chan int, 1)    //如果没有缓冲区,输出失败,发生死锁

c1 <- 1           //箭头对着channel---存
fmt.Println(<-c1) //箭头反着channel---取    1

如果没有缓冲区,输出失败,发生死锁:

  • 不给缓冲区,是没有容量的,只有在取得时候发现我要取一个,于是有了容量,它需要找个地方往里存 等着,但是程序已经不能往回执行啦

没有缓冲区的channel怎么去拿到值呢?

//无缓冲区解决死锁
c1 := make(chan int)

go func() {
   c1 <- 1
}()

fmt.Println(<-c1) //箭头反着channel---取

原理

  • 无缓冲区存10个
	c1 := make(chan int)
	
	go func() {
		for i:=0;i<10;i++ {
			c1 <- i			//无缓存区,没法存,由于有协程  用到的时候过来存
		}
	}()

	for i:=0;i<10;i++ {
		fmt.Println(<- c1)    //执行一次到这里 取的时候发现没有后,阻塞等待 从上面存一次在取
	}
  • 10个缓存区存10个

    c1 := make(chan int,10) //10个缓存区

    go func() {
    for i:=0;i<10;i++ {
    c1 <- i //一口气存完
    }
    }()

    for i:=0;i<10;i++ {
    fmt.Println(<- c1) //一口气取完
    }

  • 5个缓冲区存十个

c1 := make(chan int,5)   //5个缓存区,不够存

go func() {
	for i:=0;i<10;i++ {
		c1 <- i             //一口气存完5个,然后一直保持满的状态(取一个存一个,存完后一直取)
	}
}()

for i:=0;i<10;i++ {
	fmt.Println(<- c1)    
}

3.可读可取

c := make(chan int)

4.可读

var readChan <- chan int =c

5.可取setChan(写writeChan)

var writeChan chan <- int = c
c1 := make(chan int, 5)
var readc <-chan int = c1 //属于c1仅仅可读取的一个chan
var write chan<- int = c1
write <- 1 //只能往里写

//<- readc  //只能往外读
fmt.Println(<-readc)  // 1
2.2、channel是可以关闭的
  • channel 开启以后是可以close的 当你觉得不再需要并且已经set完成的时候 你就需要去close它
  • 此时需要注意 如果用到了 range 则必须在range之前就给它关闭
//chan可以关闭
c1 := make(chan int, 5)
c1 <- 1
c1 <- 2
c1 <- 3
close(c1)  //关闭后  到这后面就不能存了
c1 <- 4
c1 <- 5

fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
//chan可以关闭
c1 := make(chan int, 5)
c1 <- 1
c1 <- 2
c1 <- 3
close(c1)  


fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
//死锁  到这就没值了,等着往里面塞  
fmt.Println(<-c1)
fmt.Println(<-c1)
//chan可以关闭
c1 := make(chan int, 5)
c1 <- 1
c1 <- 2
c1 <- 3
c1 <- 4
c1 <- 5
close(c1) //关闭后  才能使用后面的range

for v := range c1 {
   fmt.Println(v)
}
2.3、select的用法:
  • 只要后面的可执行,就会去执行
  • 避免了使用channel时的死锁现象
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
//fmt.Println(<-ch1) //死锁
select {
case <-ch1:
   fmt.Println("ch1")
case <-ch2:
   fmt.Println("ch3")
case <-ch3:
   fmt.Println("ch3")
default:
   fmt.Println("都不满足")
}

都满足—随机执行

ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
//fmt.Println(<-ch1) //死锁
ch1 <- 1
ch2 <- 1
ch3 <- 1

select {
case <-ch1:
   fmt.Println("ch1")
case <-ch2:
   fmt.Println("ch3")
case <-ch3:
   fmt.Println("ch3")
default:
   fmt.Println("都不满足")
}
2.4、channel和goroutine交互

两个go函数的通信

func SetChan(write chan<- int) {
   for i := 0; i < 10; i++ {
      //fmt.Println("我在set函数里面")
      write <- i
   }
}

func GetChan(read <-chan int) {
   for i := 0; i < 10; i++ {
      fmt.Printf("我在get函数里面,取出了一个信息他是 %d\n", <-read)
   }
}

func main() {

   c := make(chan int)
   var read <-chan int = c
   var write chan<- int = c

   go SetChan(write)

   GetChan(read)
}
我在get函数里面,取出了一个信息他是 0
我在get函数里面,取出了一个信息他是 1
我在get函数里面,取出了一个信息他是 2
我在get函数里面,取出了一个信息他是 3
我在get函数里面,取出了一个信息他是 4
我在get函数里面,取出了一个信息他是 5
我在get函数里面,取出了一个信息他是 6
我在get函数里面,取出了一个信息他是 7
我在get函数里面,取出了一个信息他是 8
我在get函数里面,取出了一个信息他是 9

十一、断言和反射Assertion、reflect

1、断言

把一个接口类型指定为它原始的类型

type User struct {
   Name string
   Age  int
   Sex  bool
}

type Student struct {
   Class string
   User
}

func (u User) SayName(name string) {
   fmt.Println("我的名字叫做", name)
}

func check(v interface{}) {
   switch v.(type) {
   case User:
      fmt.Println("我是User", v.(User).Name)
   case Student:
      fmt.Println("我是Student", v.(Student).Class)
   }
}

func main() {

   //s := Student{
   // Class: "三年二班",
   // User:  User{},
   //}
   //check(s)  //我是Student 三年二班

   u := User{
      Name: "奇妙",
      Age:  18,
      Sex:  true,
   }
   check(u)  //我是User 奇妙

2、反射

官方说法:在编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制,称为反射。
通俗一点就是:可以知道本数据的原始数据类型和数据内容,方法等、并且可以进行一定操作。不知道原始的数据类型就可以改变原始的数据、操作原始的值

2.1、为什么要用反射
  • 我们通过接口或者其他的方式接收到了类型不固定的数据的时候需要写太多的swatch case 断言代码此时代码不灵活且通用性差
  • 反射这时候就可以无视类型改变原数据结构中的数据
2.2、反射的用法和主要函数
  • 获取输入参数接口中的数据的值
reflect.ValueOf()
  • 动态获取输入参数接口中的值的类型
reflect.TypeOf()
  • 用来判断类型
reflect.TypeOf().Kind()
  • 用来获取值
reflect.ValueOf().Fidle(int)
  • 层级取值
reflect.FieldByIndex([]int{1,0})
//取出第2个层级的第1个值 
{"你好",{"你好2","你好3"}}  //你好2
  • 获取原始数据并操作
reflect.ValueOf().Elem()

示例

type User struct {
   Name string
   Age  int
   Sex  bool
}

type Student struct {
   Class string
   User
}

func (u User) SayName(name string) {
   fmt.Println("我的名字叫做", name)
}

func check(inter interface{}) {
	t := reflect.TypeOf(inter)	//动态获取输入参数接口中的值的类型
	v := reflect.ValueOf(inter)  //获取输入参数接口中的数据的值

	for i := 0; i < t.NumField(); i++ {		//值得个数
		fmt.Println(v.Field(i))		//用来获取值
	}
	fmt.Println(t, v)
}

func main() {
	u := User{
		Name: "奇妙",
		Age:  18,
		Sex:  true,
	}
	check(u)
}
奇妙
18                      
true                    
main.User {奇妙 18 true}
func check(inter interface{}) {
   t := reflect.TypeOf(inter)
   v := reflect.ValueOf(inter)


   fmt.Println(t, v)
   fmt.Println(v.FieldByIndex([]int{0}))
   fmt.Println(v.FieldByName("Class"))
//main.Student {三年二班 {奇妙 18 true}}
//三年二班
//三年二班
}

func main() {

   u := User{
      Name: "奇妙",
      Age:  18,
      Sex:  true,
   }

   s := Student{
      Class: "三年二班",
      User:  u,
   }
   check(s) 
}

reflect.TypeOf().Kind()

func check(inter interface{}) {
   t := reflect.TypeOf(inter)
   //v := reflect.ValueOf(inter)
   ty := t.Kind()  //Struct
   if ty == reflect.Struct {
      fmt.Println("我是Struct", t)
   }
}

通过反射修改原始数据

func check(inter interface{}) {
   v := reflect.ValueOf(inter)
   e := v.Elem() //拿到原始数据
   e.FieldByName("Class").SetString("四年级二班")
   fmt.Println(inter)

   fmt.Println(v, e)
}
	//&{四年级二班 {奇妙 18 true}}
	//&{四年级二班 {奇妙 18 true}} {四年级二班 {奇妙 18 true}}

func main() {

   u := User{
      Name: "奇妙",
      Age:  18,
      Sex:  true,
   }

   s := Student{
      Class: "三年二班",
      User:  u,
   }
   check(&s) //注意传入的是地址(想要改变原始值,需要根据地址改变)
}

通过反射调用方法

func check(inter interface{}) {
   v := reflect.ValueOf(inter)
   //fmt.Println(v.Method(0))  //0xfe99e0
   m := v.Method(0)
   m.Call([]reflect.Value{reflect.ValueOf("大大")})
}

func main() {

   u := User{
      Name: "奇妙",
      Age:  18,
      Sex:  true,
   }
   check(u)
   //我的名字叫做 大大
}

十二、sync包的使用

1、Mutex 互斥锁

  • Lock() // 写的时候 排斥其他的写锁和读锁
  • Unlock()
func SyncClass() {
   l := &sync.Mutex{} //注意一定要用指针,避免不是同一个锁起不到效果
   go lockFun(l)
   go lockFun(l)
   go lockFun(l)
   go lockFun(l)
   for {
   }
}

//互斥锁,在解开之前别人用不了
func lockFun(lock *sync.Mutex) {
   lock.Lock()
   fmt.Println("疯狂刮痧")
   time.Sleep(1 * time.Second)
   lock.Unlock()
}

func main() {
   SyncClass()
   fmt.Println("111")
}
//疯狂刮痧
//疯狂刮痧
//疯狂刮痧
//疯狂刮痧
//... 不输出,等待

2、RWMutex 读写互斥锁

  • Lock() // 写的时候 排斥其他的写锁和读锁
  • Unlock()
  • Rlock() // 在读取的时候 不会阻塞其他的读锁 但是会排斥掉写锁
  • Runlock()
  1. 在读取的时候不会排斥其他的读锁,但是会排斥掉写锁—确保写完之后才能读,数据不被串改
  2. 写的时候排斥其他的写锁和读锁
func SyncClass() {
   l := &sync.RWMutex{} //注意一定要用指针,避免不是同一个锁起不到效果
   go lockFun(l)
   go lockFun(l)
   go readLockFun(l)
   go readLockFun(l)
   go readLockFun(l)
   go readLockFun(l)
   for {
   }
}

func lockFun(lock *sync.RWMutex) {
   lock.Lock() //写的时候排斥其他的写锁和读锁
   fmt.Println("疯狂刮痧")
   time.Sleep(1 * time.Second)
   lock.Unlock()
}

//读写锁
func readLockFun(lock *sync.RWMutex) {
   lock.RLock()   //在读取的时候不会排斥其他的读锁,但是会排斥掉写锁---确保写完之后才能读,数据不被串改
   fmt.Println("疯狂治疗")
   time.Sleep(1 * time.Second)
   lock.RUnlock()
}

func main() {
   SyncClass()
   fmt.Println("111")
}
//疯狂刮痧
//疯狂治疗
//疯狂治疗
//疯狂治疗
//疯狂治疗
//疯狂刮痧

3、Once

Once.Do(一个函数) 这个方法无论被调用多少次 这里只会执行一次

func SyncClass() {
   o := &sync.Once{}

   for i := 0; i < 10; i++ {
      o.Do(func() {
         fmt.Println(i)
      })
   }
}

func main() {
   SyncClass()
}
//0

4、WaitGroup

  • Add(delta int) 设定需要Done多少次
  • Done() Done一次+1
  • Wait() 再到达Done的次数前一直阻塞

案例

什么也不输出:等了5秒什么也没等到,就直接结束了

//WaitGroup
func SyncClass() {
   //wg := sync.WaitGroup{}

   go func() {
      time.Sleep(8 * time.Second)
      fmt.Println(8)
   }()

   go func() {
      time.Sleep(6 * time.Second)
      fmt.Println(6)
   }()

   time.Sleep(5 * time.Second)
}

func main() {
   SyncClass()
}
//什么也不输出

6秒输出6,8秒输出8,等待结束后输出后面的

//WaitGroup
func SyncClass() {
   wg := sync.WaitGroup{}
   wg.Add(2) //执行两次go func
   go func() {
      time.Sleep(8 * time.Second)
      wg.Done()
      fmt.Println(8)
   }()

   go func() {
      time.Sleep(6 * time.Second)
      wg.Done()
      fmt.Println(6)
   }()

   wg.Wait()
   //等到之后打印: hp0  你挂了
   fmt.Println("hp为0  你挂了")
   //time.Sleep(5 * time.Second)
}

func main() {
   SyncClass()
}

5、Map 一个并发字典

  • Store

    m := &sync.Map{}
    m.Store(1, 1) //存   
    
  • Load

    m.Load(1)   //读取
    
  • LoadOrStore

    m.LoadOrStroe(3,3)  //读的时候如果不存在,存进去
    
  • Range

  • func SyncClass() {
       m := &sync.Map{}
       m.Store(1, 1)
       m.Store(2, 2)
       m.Store(3, 3)
    
       m.Range(func(key, value interface{}) bool {
          fmt.Println(key, value)
          time.Sleep(1 * time.Second)
          return true  //false终止循环,true将继续循环
       })
    //1 1      2 2      3 3
    }
    
  • Delete

    m.Delete(1)  //删除
    

案例

func SyncClass() {
   m := make(map[int]int)

   go func() {
      m[1] = 1
   }()

   go func() {
      fmt.Println(m[1])
   }()

   for {

   }
}
// 1

fatal error: concurrent map read and map write

读写混乱,有时候同时执行啦(并发),读出来也没什么用,直接报错 panic

func SyncClass() {
   m := make(map[int]int)
   go func() {
      for {
         m[1] = 1
         //m.Store(1, 1)
         //fmt.Println(22)
      }
   }()

   go func() {
      for {
         fmt.Println(m[1])
      }
   }()

   for {

   }
}

sync.Map{} 解决读写并发

有锁的map 存和取是有锁挂着的,没有办法造成读写冲突

func SyncClass() {
   m := &sync.Map{}

   go func() {
      for {
         m.Store(1, 1)
      }
   }()

   go func() {
      for {
         //fmt.Println(m[1])
         fmt.Println(m.Load(1))
         //fmt.Println(11)
      }
   }()

   time.Sleep(100)
}

6、Pool 并发池

  • Put
  • Get
//并发池
func SyncClass() {
   p := &sync.Pool{}

   p.Put(1)
   p.Put(2)
   p.Put(3)
   p.Put(4)
   p.Put(5)
   for i := 0; i < 5; i++ {
      time.Sleep(1 * time.Second)
      fmt.Println(p.Get())
   }
}
//2
//1
//3
//5
//4
//如果i>池子里的,后面输出nil

7、Cond 没多大用的通知解锁

  • NewCond(lock) 创建一个cond
  • co.L.Lock() 。。。。 co.L.Unlock() 创建一个锁区间 在区域内部可以co.wait()
  • co.roadcast()解锁全部
  • co.Signal()解锁一个
//Cond 没多大用的通知解锁
func SyncClass()  {
   co := sync.NewCond(&sync.Mutex{})
   co.L.Lock()
   
   co.Wait()     //锁和解锁之间有一个等待
   co.Wait()     //锁和解锁之间有一个等待
   co.Wait()     //锁和解锁之间有一个等待
   
   co.L.Unlock()
   
   co.Signal()  //通知解锁一个
   
   co.Broadcast()   //通知解锁全部
}
func SyncClass() {
   co := sync.NewCond(&sync.Mutex{})
   go func() {
      co.L.Lock()
      fmt.Println("lock1 ")
      co.Wait()
      fmt.Println("Unlock1 ")
      co.L.Unlock()
   }()

   go func() {
      co.L.Lock()
      fmt.Println("lock2")
      co.Wait()
      fmt.Println("Unlock2")
      co.L.Unlock()
   }()

	//lock2    
	//lock1
	//Unlock2
	//Unlock1					//输出顺序
   time.Sleep(2 * time.Second)  //隔两秒
   //co.Broadcast()
   co.Signal()
   time.Sleep(1 * time.Second)  //隔一秒
   co.Signal()
   time.Sleep(1 * time.Second)  //隔一秒
}

十三、golang 基础之 文件操作

1、io包基础接口

1.1、Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
  • 将 len§ 个字节读取到 p 中
  • ReadFrom() 常用方法 实现了Reader接口的都可以用
func file() {
   f, err := os.OpenFile("./test.txt", os.O_CREATE|os.O_RDWR, 0777)
   if err != nil {
      fmt.Println(err)
      return
   }
   for {
      b := make([]byte, 12)
      n, err := f.Read(b)
      if err != nil {
         fmt.Println(err)
         return
      }
      fmt.Println(string(b), n)
   }
}
你好手机 12
卡辜负了 12
卡价格的 12
安徽省的 12
风 3       
EOF 
1.2、Writer
type Writer interface {
Write(p []byte) (n int, err error)
}
  • Write 方法用于将 p 中的数据写入到对象的数据流中

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9eZ5q6bJ-1670809002573)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209163304871.png)](https://img-blog.csdnimg.cn/5a4ab4f2c6ea4edea837b91605004e81.png)

1.3、Seeker
type Seeker interface {
Seek(offset int64, whence int) (ret int64, err error)
}
  • whence 为 0:表示从数据的开头开始移动指针
    whence 为 1:表示从数据的当前指针位置开始移动指针
    如果 whence 为 2:表示从数据的尾部开始移动指针
  • SeekStart = 0 // seek relative to the origin of the file
    SeekCurrent = 1 // seek relative to the current offset
    SeekEnd = 2 // seek relative to the end
  • offset 是指针移动的偏移量
1.4、Closer
type Closer interface {
Close() error
}
  • Close 一般用于关闭文件,关闭连接,关闭数据库等

2、一些常量

const (
O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
O_RDWR int = syscall.O_RDWR // 读写模式打开文件
O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件
O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
)

3、读取文件的几个关键方法

  • os.openFile() 用于打开文件 获取到 *fire

  • bufio.newReader(f) 将文件变化为 reader

  • reader.ReadString(‘字符’) 调用reader上的方法 还有 ReadLine ReadByte ReadSlice 等

    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IZKWCiSx-1670809002573)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209163624400.png)]

  • ioutil.ReadAll(f) 直接读取整个文件 os.ReadFile(文件路径) 也能达到同样效果

    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sfBdd0zM-1670809002574)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209164110980.png)]

  • os.ReadDir(“./”) 读取文件夹 获取目标文件夹下的文件信息

    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VTTSdBd7-1670809002574)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209165308235.png)]

4、写文件的几个关键方法

  • os.openFile() 用于打开文件 获取到 *fire

  • f.Seek() 挪光标位置

  • f.WriteString() 直接写入

  • bufio.NewWriter(f) 创建一个缓存的写

    • writer.WriteString() 先写入内存
    • writer.Flush() 缓存内容生效 写入文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDU7xOzi-1670809002575)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221202085210467.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0MbZDV6-1670809002575)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209225502370.png)]

5、复制文件

open 两个文件 然后 io.Copy(dst, src)

只是做了覆盖操作,从头写的操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HCOyvMlC-1670809002575)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209173101544.png)]

十四、net 包

1、tcp

通讯层协议

1.1、客户端
net.DialTCP("tcp",nil,tcpAddr)
func main() {
	tcpAddr, _ := net.ResolveTCPAddr("tcp", ":8888")
	conn, _ := net.DialTCP("tcp", nil, tcpAddr)
	//conn.Write([]byte("申请出战"))

	reader := bufio.NewReader(os.Stdin)
	for {
		bytes, _, _ := reader.ReadLine()
		conn.Write(bytes)
		rb := make([]byte, 1024)
		rn, _ := conn.Read(rb)
		fmt.Println(string(rb[0:rn]))
	}
}
//客户端输入,服务端返回
你好
收到了:  你好
你好呀
收到了:  你好呀
1.2、服务端
net.ListenTCP(协议,addr)
func main() {
	tcpAddr, _ := net.ResolveTCPAddr("tcp", ":8888")
	listener, _ := net.ListenTCP("tcp", tcpAddr)

	for {
		conn, err := listener.AcceptTCP()
		if err != nil {
			fmt.Println(err)
			return
			// handle error
		}
		go handleConnection(conn)
	}

}

func handleConnection(conn *net.TCPConn) {
	for {
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			fmt.Println(err)
			break
		}

		fmt.Println(conn.RemoteAddr().String() + string(buf[0:n]))
		str := "收到了:  " + string(buf[0:n])
		conn.Write([]byte(str))
	}
}
//客户端输入,服务端接收
127.0.0.1:55257你好
127.0.0.1:55257你好呀

2、http

应用层协议,超文本传输协议

2.0、重要的类型和接口
  • server 服务 包含地址 端口 处理器 等等等
  • conn 链接 用户请求来的
  • response 返回信息
  • request 用户的请求信息
  • Handle 对于接收的信息进行处理并且返回的一个处理器
2.1、服务端
  • Handle 和 HandleFunc

    • handle 接口
    • handleFUnc (路径,方法(res,req))
  • // 创造一个handle
    func handle (res http.ResponseWriter, req *http.Request){
    fmt.println(res, "hello world")
    }
    
  • http.HandleFunc("/",handle)
    // 把handle创建进入默认路由器中
    
  • NewServeMux 创建我们自己的MUX 路由器

  • 通过 Header() 来设置头map
    通过 WriteHeader(code) 来设置状态码
    通过 Write写入这个io.writer 来控制返回值
    通过 req.Body 以获取请求过来的body内容
    剩下的就是组装了 方法很多 不再一一示例 当然还有一些内置方法 比如把路径当静态显示

    http.Handle("/",http.FileServer(http.Dir("/")))
    

案例

func handler(res http.ResponseWriter, req *http.Request) {
   //res.Write([]byte("我收到了给你返回"))
   switch req.Method {
   case "GET":
      res.Write([]byte("我收到了给你返回GET"))
      break
   case "POST":
      b, _ := io.ReadAll(req.Body)
      fmt.Println(string(b))
	  //header := res.Header()
	  //header["test"] = []string{"test1, test2"} //处理请求头
	  fmt.Println(req.Header["Test"])
      res.WriteHeader(http.StatusBadRequest)    //设置返回状态码
      res.Write(b)
      break
   }
}

func main() {
   //等效于下面三行
   //http.HandleFunc("/test", handler) 
   //http.ListenAndServe(":8080", nil)
   
   mux := http.NewServeMux()
   mux.HandleFunc("/test", handler)
   http.ListenAndServe(":8080", mux)
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Fv2vXFw-1670809002576)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221209165108698.png)]

2.2、客户端

分三步

  • 创建client := &http,Client{};
  • 构造请求 requset,err := http.newRequest(“方法”,“路径”,body)
  • 发送得到返回 res,err := client.Do(request)
  • res 处理就好了 分析下参数

案例

func main() {
   //1.创建客户端
   client := new(http.Client)
   //2.构造请求 GET
   //req, _ := http.NewRequest("GET", "http://localhost:8080/test", nil)  //我收到了给你返回GET
   //2.构造请求 POST
   req, _ := http.NewRequest("POST", "http://localhost:8080/test", bytes.NewBuffer([]byte("{\"test\":\"我是客户端\"}"))) //{"test":"我是客户端"}
   req.Header["Test"] = []string{"test1,test2"}
   res, _ := client.Do(req)
   body := res.Body
   b, _ := io.ReadAll(body)
   fmt.Println(string(b))
}

3、rpc ***

远程过程调用,是可以本地调用远程的一个方法,理解成一个api

基于TCP或者HTTP创建的一个服务

3.1、注意事项
  • Go 的RPC只支持go写的系统
  • Go RPC的函数有特殊要求:
    • 函数首字母必须大写
    • 必须只有两个参数 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
    • 函数还要有一个返回值error
  • 举例 func (t *T) MethodName(argType T1, replyType *T2) error
3.2、服务端
  • rpc.Regist(new(符合rpc的结构体))
  • rpc.HandleHTTP() 借用http协议来作为rpc载体
  • net.Listen(“tcp”,“:1234”) 创建一个lesten
  • http.Serve(l,nil) 启动服务

案例

type Server struct {
}

type Req struct {
   NumOne int
   NumTwo int
}

type Res struct {
   Num int
}

func (s *Server) Add(req Req, res *Res) error {
   time.Sleep(5 * time.Second) //使用Call会等待这个执行完才能继续向下执行
   res.Num = req.NumOne + req.NumTwo
   return nil
}

func main() {

   //1.先把一个服务注册进入rpc
   rpc.Register(new(Server))
   rpc.HandleHTTP()
   l, err := net.Listen("tcp", ":8888")
   if err != nil {
      fmt.Println("你玩了")
   }
   http.Serve(l, nil)
}
3.3、客户端
  • 客户端一定要做好出入参结构 需要关注的是结构内部的参数 是否与rpc提供的对应 入参需要对应 出参要啥就生命啥
  • 先创建client 使用方法 rpc.DialHttp(“协议 服务器是啥这就是啥”,地址)
  • clinet.Go(结构体名.方法名,入参,回参指针,chan 可以走nil 默认即可)
    • 返回一个chan 自行创建阻塞时间
  • client.Call((结构体名.方法名,入参,回参指针)
    • 直接进行阻塞

案例

  • call
//拿过来,入参出参非常严格
type Req struct {
   NumOne int
   NumTwo int
}

type Res struct {
   Num int
}

func main() {
   req := Req{
      NumOne: 1,
      NumTwo: 2,
   }
   var res Res

   client, err := rpc.DialHTTP("tcp", "localhost:8888")
   if err != nil {
      log.Fatal("dialing:", err)
   }
   //client.Call("Server.Add", req, &res)
   ca := client.Go("Server.Add", req, &res, nil)
   fmt.Println("我可以做好多事情") //立即输出,等待后进行下面的代码
   <-ca.Done               //等待
   fmt.Println(res)

}
  • Go
//拿过来,入参出参非常严格
type Req struct {
   NumOne int
   NumTwo int
}

type Res struct {
   Num int
}

func main() {
   req := Req{
      NumOne: 1,
      NumTwo: 2,
   }
   var res Res

   client, err := rpc.DialHTTP("tcp", "localhost:8888")
   if err != nil {
      log.Fatal("dialing:", err)
   }
   //client.Call("Server.Add", req, &res)
   ca := client.Go("Server.Add", req, &res, nil)
   //fmt.Println("我可以做好多事情")  //立即输出,等待后进行下面的代码
   //<-ca.Done   //等待
   for {
      select {
      case <-ca.Done:
         //fmt.Println("我可以做好多事情")
         fmt.Println(res)
         return
      default:
         time.Sleep(1 * time.Second)
         fmt.Println("我等着呢")
      }
   }
   //client.Go()
}

写程序关键的是思维而不是使用

十五、泛型

interface所有实现类都可以传进去,不能在运行时约束具体类型

1、泛型基本含义

  • 写法: [泛型标识 泛型约束] [T any]

  • 含义:
    在定义函数(结构等)时候,可能会有多种类型传入,真正使用方法的时候才可以确定用的是什么类型,此时就可以用一个更加宽泛的类型(存在一定约束,只能在哪些类型的范围内使用)暂时占位。这个类型就叫泛型。

  • func test[T any](i T) T {
    	return i
    }
    
    func main() {
    	fmt.Println(test[string]("123" + "456"))
    	fmt.Println(test[int](123 + 456))
    	fmt.Println(test("你好" + "123"))
    }
    123456
    579    
    你好123
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTIxjr6G-1670809002576)(https://gitee.com/huizai-175/images-typora/raw/master/img1/image-20221210093259871.png)]

2、泛型方法

func F[T any](t T)(z T){

}
type My[A any] struct {
   Name string
}

func main() {
   m := My[string]{Name: "奇妙"}
   fmt.Println(m)
}

3、泛型结构体

type User[T any] struct{
Name T
}

4、泛型map

需要注意key必须要先定义约束才可使用

//创建自己的约束
type MyType interface {
   string | int | bool
}
//map的k需要约束
type myC[K MyType,V any] map[K]V

func main() {
	c := make(myC[string,int])
	c["你好"]=2
	fmt.Println(c)	//map[你好:2]
}

5、泛型切片

类似于泛型结构体

type MyQ[A any] []A
func main() {
	mq := MyQ[string]{"我是切片","你好呀"}
	fmt.Println(mq)
}
[我是切片  你好呀]

6、创建泛型约束

  • type MyType interface{
     ~int | ~int8
    }
    
  • 同样可以通过自己实现接口来实现约束

    • ype z interface {
      GetValue() string
      }
      
      type User struct {
      Name string
      }
      
      func (u User) GetValue() string {
      return u.Name
      }
      
      func test[T z](t T)T{
      fmt.Println(t.GetValue())
      return t
      }
      
      func main(){
      u :=User{Name: "sss"}
      z := test[User](u)
      fmt.Println(z.Name)
      }
      

      案例

      type MyType interface {
         getValue() string
      }
      
      func test[T MyType](t T) {
         fmt.Println(t.getValue())
      }
      
      type my struct {
         Name string
      }
      
      func (m my) getValue() string {
         return m.Name
      }
      
      func main() {
         m := my{Name: "奇妙"}
         test[my](m)
      }
      
      type MyType interface {
         //int | string
         getValue() string
      }
      
      func test[T MyType](t T) {
         fmt.Println(t.getValue())
      }
      
      type my struct {
         Name string
      }
      
      type myString string
      
      func (m my) getValue() string {
         return m.Name
      }
      
      func (m myString) getValue() string {
         return "456"
      }
      
      func main() {
         var a myString
         a = "123"
         //m := my{Name: "奇妙"}
         //test[my](m)
         test[myString](a)//456
      }
      
  • type+接口混合使用

7、~的含义

模糊匹配,所有底层为这个类型的类型

type MyType interface {
   ~int | ~int64
}

type MyInt int         //使用 ~ 使MyInt  也属于MyType约束里面

func Test[T MyType](s T) {
   fmt.Println(s)
}

func main() {
   Test[MyInt](123)  //不用~ 这里不能[MyInt],不能使用衍生结构
}

8、泛型结构体的泛型方法

type MyType[T any] struct {

}

func (m MyType[T]) name(t T) (z T) {

}

案例

type my[T any] struct {
   Name string
}

//T传递过来
func (m my[T]) getValue(t T) T {
   return t
}

func main() {
   m := my[string]{Name: "奇妙"}
   fmt.Println(m.getValue("123" + "456"))
   d := my[int]{Name: "奇妙"}
   fmt.Println(d.getValue(123 + 456))
}
123456
579

9、总结

9.1、人家提供的泛型约束
//泛型1.18
//1.泛型方法
func same[T int | float64 | string](a, b T) bool {
   return a == b
}

//2.泛型结构体
type Person[T any] struct {
   Name string
   Sex  T
}

//3.泛型map
//type TMap[K string | int, V string | int] map[K]V //k和v的类型都不确定
type TMap[K comparable, V string | int] map[K]V //官方内置的可比较类型

//4.泛型切片
type TSlice[S any] []S

func main() {
   //1.泛型方法
   b := same[float64](1.0, 1.3)
   fmt.Println(b)

   //2.泛型结构体
   p := Person[int]{
      Name: "你好",
      Sex:  14,
   }
   fmt.Println(p)

   //3.泛型map
   m := make(TMap[string, int])
   m["123"] = 123
   fmt.Println(m)

   //4.切片泛型
   //s := TSlice[string]{"你好", "123"}
   s := make(TSlice[string], 6)
   s[5] = "你好"
   fmt.Println(s)
}
false
{你好 14}   
map[123:123]
[     你好]
9.2、定义自己的泛型约束
//1.自定义泛型
/*type MyType interface {
   int | float64 | string | int32 | int64
}

func Test[T MyType](s T) {
   fmt.Println(s)
}*/

//2.实现接口的形式作为泛型使用
/*
type MyType interface {
   Get()
}

type MyT struct {
   Name string
}

func (m MyT) Get() {

}

func Test[T MyType](s T) {
   fmt.Println(s)
}

func main() {
   //2.实现接口的形式作为泛型使用
   m := MyT{Name: "奇妙"}
   //直接把m丢进来,因为它的约束是一个接口,MyT又实现了这个接口
   Test(m)
}
*/

//3.~
type MyType interface {
   ~int | ~int64
}

type MyInt int         //使用 ~ 使MyInt  也属于MyType约束里面

func Test[T MyType](s T) {
   fmt.Println(s)
}

func main() {
   Test[MyInt](123)  //不用~ 这里不能[MyInt],不能使用衍生结构
}

十六、websocket

GOPROXY=“https://goproxy.cn,direct”

http交互的升级

1.创建websocket服务端

  • 升级结构
websocket.Upgrader{
HandshakeTimeout: o,//握手时间0为不限制
ReadBufferSize:1024,//以字节为单位的IO缓冲区,如果缓冲区大小为零,则使用HTTP服务器分配的缓冲区WriteBufferSize:1024,//以字节为单位的IO缓冲区,如果缓冲区大小为零,则使用HTTP服务器分配的缓冲区WriteBufferPool: nil. / / WriteBufferPool是用于写操作的缓冲池。
Subprotocols: nil,//按顺序指定服务器支持的协议
Error: nil,//指定用于生成HTTP错误响应的函数。
CheckOrigin: nil, //对过来的请求做校验用的,
EnableCompression: false,//指定服务器是否应尝试根据进行协商消息压缩
}
  • 主要函数Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header)(*Conn, error)

    • 会获得一个conn链接这个链接可以再通过后续的注册请求来做很多的业务相关具体设计要看你们的业务如何判断

    • conn上面最主要的方法为读ReadMessage和写WriteMessage 从而实现websocket的交互

    • 读和写第一个参数都是message类型这里文档有标记

    • 此处为统一规范

      //TextMessage denotes a text data message.The text message payload is
      // interpreted as UTF-8 encoded text data.
      TextMessage = 1
      
      // BinaryMessage denotes a binary data message.
      BinarvMessaae = 2
      

server

var UP = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

var conns []*websocket.Conn

func handler(res http.ResponseWriter, req *http.Request) {
	conn, err := UP.Upgrade(res, req, nil)
	if err != nil {
		log.Println(err)
		return
	}
	conns = append(conns, conn)
	for {
		m, p, e := conn.ReadMessage()
		if e != nil {
			break
		}
		for i := range conns {
			conns[i].WriteMessage(websocket.TextMessage, []byte("你说的是:"+string(p)+"吗?"))
		}

		//conn.WriteMessage(websocket.TextMessage, []byte("你说的是:"+string(p)+"吗?"))
		fmt.Println(m, string(p))
	}
	defer conn.Close()
	log.Println("服务关闭")
	//现在已经正式的从http升级为websocket
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8888", nil)

}
1 我是客户端0
1 我是客户端1

2、创建websocket客户端

客户端需要创建一个Dialer

client0

func main() {
   dl := websocket.Dialer{}
   conn, _, err := dl.Dial("ws://127.0.0.1:8888", nil)
   if err != nil {
      log.Println(err)
      return
   }
   go send(conn)
   for {
      m, p, e := conn.ReadMessage()
      if e != nil {
         break
      }
      fmt.Println(m, string(p))
   }
}

func send(conn *websocket.Conn) {
   for {

      reader := bufio.NewReader(os.Stdin)
      l, _, _ := reader.ReadLine()
      conn.WriteMessage(websocket.TextMessage, l)
   }
}
我是客户端0
1 你说的是:我是客户端0吗?
1 你说的是:我是客户端1吗?

client1

func main() {
   dl := websocket.Dialer{}
   conn, _, err := dl.Dial("ws://127.0.0.1:8888", nil)
   if err != nil {
      log.Println(err)
      return
   }
   go send(conn)
   for {
      m, p, e := conn.ReadMessage()
      if e != nil {
         break
      }
      fmt.Println(m, string(p))
   }
}

func send(conn *websocket.Conn) {
   for {

      reader := bufio.NewReader(os.Stdin)
      l, _, _ := reader.ReadLine()
      conn.WriteMessage(websocket.TextMessage, l)
   }
}
1 你说的是:我是客户端0吗?
我是客户端1
1 你说的是:我是客户端1吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/82538.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VTK-vtkFieldData

欢迎大家加入社区&#xff0c;雪易VTK社区-CSDN社区云 前言&#xff1a;为区分vtkPoints和vtkPointData的区别&#xff0c;了解vtkFieldData在VTK中的存在意义&#xff0c;从而系统的掌握vtk中关于数据的表达方式。 vtk中通过vtkDataArray进行数据的存储&#xff0c;通过vtkD…

艾美捷内皮细胞生长添加剂参数说明和相关研究

内皮细胞生长因子&#xff08;ECGF&#xff09;或内皮细胞生长补充物&#xff08;ECGS&#xff09;是一个术语&#xff0c;也用于含有促进内皮细胞生长和维持活性的牛&#xff08;或猪&#xff09;脑提取物&#xff08;T.Maciag&#xff0c;1972和1982&#xff09;。在早期&…

SQL语法CREATE_大数据培训

1 CREATE 1.1 CREATE DATABASE 用于创建指定名称的数据库&#xff0c;语法如下&#xff1a; CREATE DATABASE [IF NOT EXISTS] db_name 如果查询中存在IF NOT EXISTS&#xff0c;则当数据库已经存在时&#xff0c;该查询不会返回任何错误。 create database test; Ok. 0 …

【虚幻引擎】UE4/UE5Map、Set、 Array的用法

一、Array Array:数组是一个内存空间连续&#xff0c;可以存储多个相同类型的有序的元序列集合。 每一个索引值对应一个Value值&#xff0c;比如0号索引值对应A&#xff0c;值可以是任意类型的变量 用法&#xff1a; 节点 描述 Add 取入一个数组和一个变量。它将该变量插入…

014. 解数独

1.题目链接&#xff1a; 37. 解数独 2.解题思路&#xff1a; 2.1.题目要求&#xff1a; 暂时的理解就是&#xff0c;编写一个程序然后自动填完数独&#xff0c;填完返回&#xff08;不用求解各种不同的数独组合&#xff09; 填的时候&#xff0c;数字要满足的规则&#xff1…

198.打家劫舍,213.打家劫舍II ,337.打家劫舍III

198. 打家劫舍 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代表每个…

项目管理中的WBS

项目管理中的WBS(工作分解结构) 在项目管理中&#xff0c;我们必然会用到一个工具WBS(工作分解结构)&#xff0c;在PMP的课程中&#xff0c;也作为了一个重要的考察对象。 一. WBS的定义 工作分解结构&#xff08;Work Breakdown Structure&#xff09;&#xff1a;以可交付成…

什么是网络分析仪噪声参数?

噪声参数在被测器件的输入端口和测试仪器内置噪声接收机的输入端口上都会产生影响。要想了解为什么噪声参数会给测量结果带来误差&#xff0c;我们首先需要了解什么是噪声参数。放大器的噪声参数描述了噪声系数随着源阻抗Γs而变化的情况。在史密斯圆图上&#xff0c;噪声参数通…

长盈通在科创板上市:研发费用率低于行业均值,皮亚斌为实控人

12月12日&#xff0c;武汉长盈通光电技术股份有限公司&#xff08;下称“长盈通”&#xff0c;SH:688143&#xff09;在上海证券交易所科创板上市。本次上市&#xff0c;长盈通的发行价为35.67元/股&#xff0c;发行市盈率48.61倍&#xff0c;而该公司所属行业最近一个月静态平…

QTabBar进阶用法:修改标题宽度,使标题宽度自适应窗体宽度,close图标大小设置,close图标修改,文字对齐方式修改

这是一个没有处理过的QTabWidget, 在功能上已经满足使用了&#xff0c;但是有时会有一些外观上特殊的需求&#xff0c;需要对它进行修改。 1. 更改标签的长度。 可以用样式表改&#xff1a; setStyleSheet("QTabBar::tab{height:50;width:200}");"QTabBar::t…

GitLab安装与卸载

一、安装Postfix以发送通知邮件 安装命令&#xff1a;sudo yum install postfix 将postfix服务设置成开机自启动&#xff0c;安装命令&#xff1a;sudo systemctl enable postfix 启动postfix&#xff0c;安装命令&#xff1a;sudo systemctl start postfix 二、安装gitlab …

【数据结构】栈定义及其常用的基本操作(C/C++)

目录 ●图示&#xff08;以顺序栈为例&#xff09; ●栈的类型定义 ●栈常用的基本操作 ●顺序栈 ●链式栈 ●简单案例 1.顺序栈&#xff08;这里只实现用顺序表存储3个学生的学号、姓名、年龄并且将其输出查看。若进行其他操作&#xff0c;对代码进行简单修改即可&…

Read IDS scan文件

IDS 雷达的文件格式比原来的。dt格式文件复杂&#xff0c;由于数据来自检测单位&#xff0c;对文件的理解并不到位。 采集的数据如下&#xff1a; 产生的文件很多&#xff0c;比如这次有2个采集 Data内部的文件 很多文件并没有理解到 3 文件说明 3.1 pos 结尾是 但距离与最后…

Mysql高频面试题(二)

文章目录1. 平衡二叉树、红黑树、Hash结构、B树和B树的区别是什么&#xff1f;2. 一个B树中大概能存放多少条索引记录&#xff1f;3. 使用B树存储的索引crud执行效率如何&#xff1f;4. 什么是自适应哈希索引&#xff1f;5. 什么是2-3树 2-3-4树&#xff1f;6. 为什么官方建议使…

边界安全防护方案

汽车制造业 MES系统 DNC系统 生产 安全域1 管理层 工控安全隔离装置 交换机 安全配置核查系统 HMI 历史数据库 运行监控系统 实时数据库 打印机 过程 安全域2 监控层 工控漏洞扫描系统 安全交换机 工控安全审计系统 工控入侵检测系统工程师站 A 操作员站 A 实时数据库A 操作员…

测试员做外包能转正吗?外包员工能变正式员工吗?

外包员工能变正式员工吗&#xff1f;这里辟谣一波&#xff0c;许多外包都说有转正机会。实际情况是几乎等于零。其中&#xff0c;三方外包更是可以直接和零划等号。三方外包的转正&#xff0c;往往就是给个内推机会&#xff0c;然后和面试官会熟悉一些。 然而这些都没什么价值…

简述基于JavaEE企业级开发技术(Spring)

一、绪论 1、学习内容 javaEE企业开发技术概述javaEE容器——SpringORM数据层——MyBatis/JPAWeb层——Spring MVC展现层——JSP/Thymeleaf整合框架——SSM/SSH用户模块分析用户模块功能模块设计 前端框架&#xff1a;Bootstrap&#xff0c;NodeJS&#xff0c;Vue/React/Ang…

聊一聊世界杯的半自动越位技术

一、前言 11 月 22 日&#xff0c;在卡塔尔卢赛尔球场进行的 2022 卡塔尔世界杯足球赛 C 组比赛中&#xff0c;阿根廷队以 1 比 2 不敌沙特阿拉伯队&#xff0c;我相信哥很多人都不敢相信&#xff0c;阿根廷居然又输了&#xff0c;而沙特取得亚洲球队在本届世界杯的首场胜利。…

【Pytorch】第 9 章 :Capstone 项目——用 DQN 玩 Flappy Bird

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

一文了解瀑布式项目管理和敏捷项目管理的区别!

众所周知&#xff0c;项目管理起源于软件开发行业&#xff0c;而目前已广泛应用于各行各业&#xff0c;完整的项目管理包含五个部分&#xff0c;分别是&#xff1a;项目启动、项目规划、项目执行、项目监控、项目收尾。随着行业的发展&#xff0c;传统的瀑布式项目管理模式&…