Go基础知识

目录

一、开发文档

Go语言官网

Go中文社区
Golang标准库文档
Go语言学习之路
比较通俗易懂的中文文档
go env那些事
添加链接描述
国外官方下载地址

二、安装

安装包下载
查看Golang版本 : go version
产看环境配置:go env
1.11版本之后不需要修改环境变量,环境变量配置

三、基础

1、包的使用

在go1.11版本之前,写项目时需要在$GOPATH的目录下;
在go1.11版本之后内部存储的时我们公共go.mod下载的依赖包,所以在写项目前要初始化mod: go mod init 文件名,会在对应的文件夹下生成go.mod文件;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2、变量var

1)自动推导类型 a:=1
2)变量交换赋值 a,b=b,a
3)匿名变量a,b,_,c=1,2,3,4
4)格式化输出 fmt

fmt.Println() // 输出数据自动带换行
fmt.Print() // 输出数据不带换行
fmt.Printf()
/* 格式化输出数据 :
%d(%3d表示保留3位整数) 整型;
%f(%.3f表示小数点后面保留三位,会对第四位四舍五入) 浮点型;
 %t 布尔类型;
 %s 字符串类型;
 %c 字符类型;
 %p 内存地址;
 %T  打印变量对应的数据类型;
 %b 表示输出二进制;
 
 %V 打印值;
 %+V 打印详细的值;
 %#V 打印更详细的值;
*/

5)格式化输入:

fmt.Scan() 
/*
输入数据 
 &变量  &取地址符号; 
空格或回车作为接受结束;
案例:
// 接收两个数据
	var c, d string
	fmt.Scan(&c, &d)
	fmt.Println(c,d)
*/
fmt.Scanf() 
/*
格式化输入数据
案例:
var f int
	var g string
	fmt.Scanf("%3d", &f)
	fmt.Scanf("%s", &g)
*/

6)命名规范:
    ①只能以字母或下划线开头
    ②只能使用字母 数字 下划线
    ③区分大小写
    ④不能使用系统关键字
    ⑤见名知义
    ⑥变量不能同时声明
    ⑦不同的数据类型之间不能进行计算

3、常量 const

1)使用 const 定义

const MAX int = 10 
/*
常量一般用大写字母表示;
常量的存储位置在数据区 不能通过& 取地址来访问;
扩展:
常量的存储位置在数据区
栈区 系统为每一个应用程序分配1M空间用来存储变量  在程序运行结束系统会自动释放
*/

2)枚举

iota 定义一个从0开始的值
案例:
   const (
		a=iota
		b=iota
		c=iota
	)
	// 0 1 2
4、运算符

与PHP不同之处:
 1)取值*把指针地址的对应的值取出来
*和&

&符号的意思是对变量取地址,如:变量a的地址是&a
*符号的意思是对指针取值,如:*&a,就是a变量所在地址的值,当然也就是a的值了
案例:
func main()  {
	a:=10
	p:=&a // 
	fmt.Println(*p)
}

 2)取地址&获取变量的地址
 3)类型转换

1int32int64都是整形,只有类型匹配的数据才能进行计算;
2、类型转换为了保证数据完整性,将低类型转换成高类型;

案例:
func main()  {
	var a int32 = 10
	var b int64 = 3
	c:=int64(a)*b
	fmt.Println(c)
}

 4)自增(++),自减(-)

区别:
1、 不能参与表达式计算,反例:b:=a++
2、 只有后自增后自减,反例:++a
案例:
func main()  {
	a:=10
	//a++ // 自增运算符
	//a-- // 自减运算符
	fmt.Println(a)
}

 5)两个相同类型的数据相除是得到的结果类型也相同

案例:
func main()  {
	a:=10
	b:=3
	fmt.Println(a/b) //3
}

 6)字符串相加 +

5、流程控制
1)if
    if a > b{
		fmt.Println(a)
	}else if a==b {
		fmt.Println(b)
	}else{
		fmt.Println(c)
	}
2)switch
案例:               
func main() {
	var a int = 0
	fmt.Println(a)

	// swich中的只不能是浮点型数据 浮点型数据是一个约等于的数据
	// 与PHP不同点,执行到所在的值后自动跳出
	switch a {
	case 0:
		//fmt.Println("星期一")
		fallthrough //与PHP不同点,让switch执行下一个分支的代码  如果不写 执行到下一个分支就会自动停止
	case 1:
		fmt.Println("星期一")
	case 2:
		fmt.Println("星期二")
	case 3, 4: // 可以是多个结果选项
		fmt.Println("星期三")
	default: // 如果输入的值没有找到 默认进入default 中
		fmt.Println("I dont now")
	}
}

3)for
案例:               
func main()  {
	// 计算1-100的和
	c:=0
	for b:=1;b<=100;b++ {
		c+=b
	}
	fmt.Println(c)

	// 与PHP不同之处:
	// 1、条件的初始值可以拿出来,当变量来使用
	// 2、结束条件也可以拿出来,进行判断,满足条件使用 break 进行返回
	// 3、i++ 也可以拿出来
	i:=0
	for ; ;  {
		if(i>5){
			break
		}
		fmt.Println(i)
		i++
	}
	fmt.Println(i)

	//for 和range 可以遍历 集合中的数据信息  数组  切片 结构体数组  map
	for _,data:=range a{
		fmt.Println(data)
	}
}

总结:

// 方式一:只遍历不关心数据,适用于切片、数组、字符串、map、channel
for range T {}

// 方式二:遍历获取索引或数组,切片,数组、字符串就是索引,map就是key,channel就是数据
for key := range T{}

// 方式三:遍历获取索引和数据,适用于切片、数组、字符串,第一个参数就是索引,第二个参数就是对应的元素值,map 第一个参数就是key,第二个参数就是对应的值;
for key, value := range T{}
4)关键字 break、continue

break 在循环中跳出循环,在嵌套循环中 跳出本层循环。
continue 结束本次循环继续下次循环

func main() {
outer:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if i > 3 {
				break outer // 结束指定层的循环
			}
			if j%2 == 0 {
				continue // 跳出循环继续执行
			}

			if i > 9 {
				break // 结束当前层的循环
			}
			fmt.Printf("i,j=%d,%d\t", i, j)
		}
	}
}
5)各流程控制之间的区别

区别:
if 可以嵌套 可以判断区间 执行效率比较低
switch 执行效率高 不能嵌套和区间判断

6、函数

参考:golang的函数func方法
  1)函数:定义、不定参数、定义返回值类型

案例:
package main

import "fmt"

func main()  {
	a:=10
	b:=20
	add(a, b)
	gets(1, 2, 3, 4, 5)
}

func add(a int, b int)  {
	fmt.Println(a+b)
}

/**
不定参函数
注意:
	1、参数的类型要统一
	2、a 是任意取的名称
 */
func gets(a ...int)  {

	// 将下标为0(包含0),以后的数据传递 编号是从0开始算的
	gets2(a[0:]...) //  [1 2 3 4 5]
	gets2(a[:3]...) //  [1 2 3]
	gets2(a[1:3]...) // [2 3]
	b:=gets3(1, 2, 3) // 设置返回值的类型 [2 3]
	fmt.Println(b)
	c,d:=gets4() //函数多个返回值
	fmt.Println(c,d)

	//return 表示函数的结束  return 后面的代码不会执行 同时return也会将函数的返回值传递给主调函数
	return
}

func gets2(a ...int)  {
	fmt.Println(a)
}

// func  函数名(形参列表)返回值类型列表{代码体}
// 可以通过返回值改变实参数据
func gets3(a int, b int, c int) (sum int) {
	sum = a+b+c
	return sum
}

//函数多个返回值
//如果函数没有参数在函数调用时()必须写
func gets4()(a int, b int)  {
	a,b=11,22
	return
}

  2)定义函数类型

案例:
// 函数类型
func main()  {

	//如果使用Print打印函数名是一个地址
	//函数名本身就是一个指针类型数据  在内存中代码区进行存储
	fmt.Println(test6)

	//自动类型推导创建函数类型
	f0:=test7
	f0(10,20)

	// 定义函数的变量
	var f FUNCTYPE
	f=test6
	// 通过函数变量调用函数
	f()

	var f1 FUNCTEST
	f1=test7
	f1(10,20)

	//直接定义函数类型
	var f11 func(int,int)
	f11=test7
	f11(10,20)

	var f2 funcdemo
	f2=test8
	fmt.Println(f2(100,200))
}


/*
type 可以定义函数类型
type 可以为已存在类型起别名
注意:
	1、所起的名字不能一样
*/
type FUNCTYPE func()
type FUNCTEST func(int, int)
type funcdemo func(int, int)int

func test6()  {
	fmt.Println("瓜娃子")
}

func test7(a int, b int) {
	fmt.Println(a+b)
}

func test8(a int, b int) (sum int) {
	sum = a+b
	return sum
}

  3)函数的作用域:局部变量、全局变量

注释:
func main()  {
	//局部变量
	// 在函数内部定义的变量,作用域限定于本函数内部,从变量定义到本函数结束有效
	// 在同一作用域范围内 变量名是唯一的

	//程序中如果出现了相同的变量名 如果本函数有自己的变量 就使用自己的 如果没有上外层寻找变量
	//如果名字相同会采用就进原则 使用自己的变量
	i:=0
	for i:=0;i<10;i++{

	}
	fmt.Println(i)

	demo2()
}

//全局变量:
//在函数外部定义的变量成为全局变量
//全局变量作用域是项目中所有文件
//全局变量在内存中数据区存储 和const定义的常量存储位置都是数据区
var a int=10//如果全局变量没有初始值值为0

//全局常量
const b int =10
func demo2(){
	a:=123
	fmt.Println(a)
}

  4)匿名函数

匿名函数:没有名称的函数
案例
func main01()  {
	a:=10
	b:=20

	// 匿名内部函数
	func (a int, b int){ // 需要传入的参数
		fmt.Println(a+b)
	}(a, b) // 函数调用,传入的值

	f:= func(a int,b int) {
		fmt.Println(a*b)
	}
	f(a, b)

	// 匿名行数的返回值
	v:= func(a int,b int)int {
		return a + b
	}(a, b)
	fmt.Println(v)
}

  5)闭包

//可以通过匿名函数和闭包 实现函数在栈区的本地化
func main()  {
	var a int
	f:= func() int{
		a++
		return a
	}

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

  6)递归

案例:
func main()  {
	// 计算 1-100的和
	sum:=test9(100, 0)
	fmt.Println(sum)
}

func test9(a int, b int)int  {
	if(a <= 0){
		return b
	}
	b += a
	a--
	return test9(a, b)
}
6.1、init函数

Go语言提供了先于main函数执行的init函数,初始化每个包后会自动执行init函数,每个包中可以有多个init函数,每个包中的源文件中也可以有多个init函数,加载顺序如下:

从当前包开始,如果当前包包含多个依赖包,则先初始化依赖包,层层递归初始化各个包,在每一个包中,按照源文件的字典序从前往后执行,每一个源文件中,优先初始化常量、变量,最后初始化init函数,当出现多个init函数时,则按照顺序从前往后依次执行,每一个包完成加载后,递归返回,最后在初始化当前包!

init函数实现了sync.Once,无论包被导入多少次,init函数只会被执行一次,所以使用init可以应用在服务注册、中间件初始化、实现单例模式等等,比如我们经常使用的pprof工具,他就使用到了init函数,在init函数里面进行路由注册:

7、工程管理
1)目录结构
|
bin  	     Go的源码文件
│
pkg             编译完成后的归档文件
│
src  	     Go的开发目录,必须以src命名
├─test1.go
├─test2.go
├─userinfo
│  ├─select.go
│  ├─delete.go
2)相关注意事项

  ①在同级别下,package 要相同
  ②同级别目录下,不需要引入包
  ③包的函数首字母要大写
  ④Goland 对中文的支持有 BUG,输入法有时会无法唤出
  ⑤Golang 需要重启才能自动引入 import

7.1 任意目录创建方法引用

问题: build command-line-arguments: cannot find module for path xxx
解决方案:
win + r 输入 cmd

go env -w GO111MODULE=auto

使用相对路径引入文件 Demo:

import (
	"./engine"
)

在这里插入图片描述

8、复合数据类型
1)数组(值传递)
1、数组元素的个数必须小于等于定义的数量;
2、数组在定义后 元素个数就已经固定 不能添加;
3、数组是一个常量 不允许赋值 数组名代表整个数组;
4、数组名也可以表示数组的首地址;
5、数组作为函数参数,是值传递,形参不会影响实参的值;

案例:
var arr [10]int =[10]int{1,2,3,4,5}
var arr1 [5]int=[5]int{1,2,3:10} // 指定数组下标进行初始化操作
brr:=[...]int{1,2,3}

声明数组(使用关键字new)var d = new([10]int)

  ① 一维数组
     定义:var 数组名 [函数的个数] 数据类型

  ②双色球(数组和随机数的应用)

// 双色球
// 规则:红球1-33选6,篮球1-16选1
func main()  {
	red:=randNum(33, 6)
	red=BubbleSort(red)
	fmt.Println(red)

	blue:=randNum(16, 1)
	fmt.Println(blue[0])
}

func randNum(a int, b int)(num [6]int)  {
	// 创建随机种子 
	rand.Seed(time.Now().UnixNano())

	for i:=0; i<b; i++ {
		num[i] = rand.Intn(a)+1 // 因为循环出来的是0-(a-1)范围的数字

		// 数组去重
		for j:=0; j < i; j++ {
			if num[j] == num[i]{
				num[i] = rand.Intn(a)+1
				j=-1
				continue
			}
		}
	}
	return num
}

func BubbleSort(arr [6]int) [6]int {
	for i:=0; i<len(arr)-1; i++ {
		for j:=0; j<len(arr)-1-i; j++ {
			if arr[j] > arr[j+1] {
				arr[j],arr[j+1] = arr[j+1],arr[j]
			}
		}
	}
	return arr
}

  ③二维数组
     定义:var 数组名 [行个数] [列个数] 数据类型

//定义赋值
var arr[3][4] int

arr[2][3] = 2
arr[1][2] = 1
fmt.Println(arr)

// 数组的初始化
var arr = [3][4]int{{1,2,3,4}, {2,3,4,5}, {3,4,5,6}}
2)切片(地址传递)

  ①定义:var 切片名称 [] 数据类型

  // 自动推导类型创建
	s1:=make([]int,5)
	//赋值
	s1[0]=123
	s1[1]=234
	s1[2]=345
	s1[3]=456
	s1[4]=567
	// 通过sppend 添加切片信息
	s1=append(s1,1,2,3,4,5)

	fmt.Println(s1)

  ②与数组的区别

1、切片可以理解为可以扩展的数组,不通点数两者在内存当中的存储的位置不同
2、不写元素个数叫切片 必须写元素个数的叫数组
var s2 =[]int{1,2,3,4,5}
3、切片扩容的时候地址可能发生改变,因为切片在内存中存放在堆区

  ③切片的容量

切片的容量:
1、容量大于等于长度
2、容量每次扩展为上次的倍数
3、如果整体数据没有超过1024字节 每次扩展为上一次的倍数  超过1024 每次扩展上一次的1/4
4、在切片打印时 只能打印有效长度中的数据 cap不能作为数组打印的条件
fmt.Println("容量:", cap(s2))

  ④切片的截取

切片的截取(前闭后开原则,前包含后不包含):
注:截取后的切片值得改变影响原切片的值
               s:=[]int{1,2,3,4,5}
	// 切片名[起始下标:]
	//s2:=s[2:]
	// 切片名[:结束位置] 不包含结束位置
	//s2:=s[:1]
	// 切片名[起始位置:结束位置]
	//s2:=s[1:3]
	// 切片名[起始位置:结束位置:容量]
	//s2:=s[0:2:2]//大于等于len  小于等于cap  len <= xxx <= cap
	// 切片名[:]获取切片中所有元素
	//s2:=s[:]
	fmt.Println(s2)

  ⑤切片的追加:append

例:s=append(s, 1,2,3)
添加数据,容量增加时,内存地址有可能发生变化

  ⑥切片的拷贝copy

s1 := []int{1,2,3,4,5}
s2 := make([]int, 5)

//将s1切片中的数据拷贝到s2中,s2中要有足够的容量
//使用拷贝操作后s1 s2是两个独立空间 不会相互影响
copy(s2, s1)
fmt.Println(s2)

s3 := make([]int, 5)
// 截取部分片段进行拷贝
copy(s3, s1[2:])
fmt.Println(s3)

  ⑦建议:在研发过程中,使用切片代替数组

3)map(地址传递)

  ①定义:var map名 map [keyType] valueType

m:=make(map[int]string,1) // map[]

  ②特点

map[key] key 是一个基本数据类型
map的长度是自动扩容的
map中的数据是无序存储的
map中key和value不能翻过来操作
map在定义时 key是唯一的  不允许重复

  ③遍历

	m:=make(map[int]string,1)
	m[12] = "张三"
	m[200] = "李四"
	m[30] = "王五"

	// map遍历
	for k,v:=range m{
		fmt.Println(k,v)
	}

  ④map:判断、删除

	m := make(map[int]string,1)
	m[1] = "1"
	m[2] = "2"
	// 1、map中值的判断
	_,ok:=m[3]
	if ok{
		fmt.Println("存在")
	}else{
		fmt.Println("不存在")
	}

	// 2、删除map中的值
	delete(m, 3)
	//delete在删除map中的元素的时候 如果可以 不存在 不会报错

	// 3、map作为函数参数
	// map作为函数参数是地址传递 (引用传递)
	// map在函数中添加数据 可以的  影响主调函数中实参的在
4)结构体

  ①定义

// 结构体
// 在函数外部定义结构体, 作用域是全局的
//type 结构体名称 struct {
//	结构体成员列表
//}

type Student struct {
	name string
	sex string
	age int
	addre string
}

func main()  {

	// 1、通过结构体名称,定义结构体变量
	var s Student
	s.name = "张三" //结构体变量名.成员名
	s.age = 28
	s.addre = "山西运城"
	s.sex = "男"
	fmt.Println(s)

	// 2、直接给结构体成员赋值
	s1:=Student{"李四","女",29,"闻喜"}
	fmt.Println(s1)
}
注:1、结构体和结构体成员可以进行比较;
2、结构体数组
var 结构体数组名 [元素个数]结构体类型
var arr [5]student
3、结构体切片
var 结构体切片名 []结构体切片
案例:
arr:=[]student{{1,"曹操","女",66,"河南"},
		{102,"夏侯惇","男",40,"荆州"}}

	arr=append(arr, student{104,"许褚","男",28,"许昌"})

	for i,v:=range arr {
		fmt.Println(i,v)
	}

  ②特点

1、结构体作为函数参数传递是值传递(具体还得看结构体参数的格式类型);
注:值传递-形参单元和实参单元是不同的存储区域,修改不影响其他值;
地址传递(引用传递)-形参单元和实参单元在相同的区域,修改影响其他值;
2、通常使用结构体来存储数据,类似面向对象的思想,也比较方便;
3、结构体的嵌套:
1)父类的结构体成员可以直接用来使用;
2)结构体成员同名,优先使用子类的结构体成员;

4.1)结构体序列化 (json序列化忽略某个字段、忽略空值字段)
package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name  string `json:"name"`
	Age   int    `json:"-"` // 忽略某个字段(不会不打印出来)
	Email string `json:"email,omitempty"` // omitempty:忽略空值字段(序列化结果是空值就会被忽略掉)
}

func main() {
	u1 := Person{
		Name:  "asong",
		Email: "z@163.com",
		Age:   18,
	}
	b, err := json.Marshal(u1)
	if err != nil {
		fmt.Printf("json.Marshal failed, err:%v\n", err)
		return
	}
	fmt.Printf("str:%s\n", b)
}
5)指针(地址传递)

  ①定义var 指针名称 *数据类型

var a int = 10

// 将a的地址赋值给指针变量p
p:=&a
fmt.Println(p)

//通过指针变量 间接访问变量对应的内存空间
*p=123 // 通过指针改变变量的值
fmt.Println(*p)
fmt.Println(a)

其它概念:
空指针:声明了一个指针 默认值为nil(空指针 值为0)指向了内存地址编号为0 的空间;
野指针:指针变量指向了一个未知的空间;

  ②创建指针空间:new(指针的类型)

var p *int
fmt.Println(p)

// 在堆区创建空间
p=new(int) //new 创建好的空间值为数据类型的默认值
fmt.Println(p) //打印p的值
fmt.Println(*p) //打印p指向空间的值

  ③数组指针,定义:var 指针名称 *[数组个数]int

var arr [5]int  = [5]int{1,2,3,4,5}
// 定义数组指向指针
var p *[5]int
//将指针变量和数组建立关系
p=&arr

// 直接使用指针[下标]操作元素
fmt.Println((*p)[0])
fmt.Println(p[0]) // go语言简写形式

for i:=0; i<len(p); i++ {
	fmt.Println(p[i])
}

  ④切片指针

注:切片指针,实际上是二级指针,存在两个指针地址;这是与数组指针根本的区别;
var arr []int=[]int{1,2,3,4,5}

var p *[]int            //二级指针
p=&arr

(*p)[1]=200//ok
//p[1]=123 //err 不能通过指针访问切片中的元素

// 通过指针添加元素
*p=append(*p,6,7,8,9)

fmt.Println(arr)
fmt.Println(*p)

  ⑤指针数组,定义:var 指针名称 [数组个数]*int

  ⑥指针切片,定义:var 指针名称 []*int

// 1、指针数组
var arr [3]*int

a:=1
b:=2
c:=3

arr[0]=&a
arr[1]=&b
arr[2]=&c

// 通过指针数组改变变量的值
*arr[1] = 20
fmt.Println(*arr[1])
// 变量指针数组对应的内存空间的值
for i:=0; i<len(arr);  i++{
	fmt.Println(*arr[i])
}

// 2、指针切片
var slice []*int
aa:=10
bb:=20
cc:=30

slice=append(slice, &aa,&bb,&cc)
for i,v:=range slice {
	fmt.Println(i, *v)
}

  ⑦结构体指针,定义:var 变量名称 * 结构体名称 ,结构体指针可以直接调用成员

9、双引号,反引号,单引号区别
1、双引号里的字符串可以转义,不能换行

2、反引号里面的内容不能转义,可以换行,一般用于SQL语句,html等大段内容,以及正则表达式的使用

3、单引号,一般只能用来包裹一个字节的ASCII码字符,例如:
var asc byte = ‘a’
fmt.Println(asc) //输出a的ASCII码值 97
10、new() 和 make()

golang的make

# 引用数据类型(map、切片、指针),在声明的时候需要初始化操作,分配内存空间

make 创建存储空间,适用于map、切片、channel的内存创建,返回的类型是本身,而非它们的指针类型;
Demo:
	var user = make(map[string]string)
	var user = make([]int, 4, 4) // 第一个参数是类型,第二个参数是分配的空间,第三个参数是预留分配空间
注:selice可以不用make初始化,mapchan必须使用make初始化
	
new 创建存储空间,适用于指针;返回的类型是它们的指针类型;
Demo:
    // 使用 new函数得到的是一个指针类型,并且该指针对应的值为该类型的零值
	var a = new(int) // a是一个指针变量,类型是 *int的指针类型,值是0
	

在这里插入图片描述
在这里插入图片描述

11、包的引入方式
import (
	// 1、报名可以省略不写
	. "fmt"
	// 2、给包取个别名
	tp "os"
	// 3、只想调用该包的init函数,不使用包导出的变量或者方法
	_ "os/exec"
)
12、…的含义

参考:Golang切片操作

golang ... 的意思是是获取切片中的所有元素(拍扁),类似于把切片中的值循环拿出来
Demo1:
func main() {
	s1 := []int{1, 2, 3, 4, 5} //短操作符声明 len为4,cap为4
	s2 := make([]int, 2, 4)    //make语法声明 ,len为2,cap为4
	s2 = []int{5, 6}

	fmt.Printf("s1类型:%T   s2类型:%T\n", s1, s2)
	s4 := append(s2, s1...) //append  一个切片所有的元素
	fmt.Println(s4)         //[5 6 3 4]
}
Demo2:
func main() {
	fmt.Println(Add([]int{1, 2, 3}...))
}

func Add(args ...int) int {
	sum := 0
	fmt.Printf("类型:%T", args)
	for _, v := range args {
		sum += v
	}
	return sum
}
13、值类型和引用类型

1)区别
值类型:内存中变量存储的是具体的值;在赋值或传参时,传递的是值的拷贝;修改对方不会影响本体;
引用类型:变量直接存放的就是一个地址值;在赋值或传参时,传递的是值的地址;修改对方会直接改变本体;
可以简单的通过赋值为nil判断一个类型是否为引用类型(能赋值为nil就是引用类型,否则为值类型
值类型的内存通常分配在栈中,引用类型的内存通常分配在堆中

栈内存由系统直接管理(操作系统自己会回收)(函数内的局部变量,使用完毕后就自己释放,其内存就是分配在栈中)
堆内存需要“自己管理” (c/c++需要程序员手动管理,Java/Golang编译器自带堆内存的垃圾回收器)

在这里插入图片描述
在这里插入图片描述

四、面向对象

1、方法的定义和使用

方法接收者,传值和传指针的区别
Go语言不是传统的面向对象;Go有结构体与方法;仅支持封装,不支持多态和继承;不论地址还是结构体本身,一律使用 . 来访问;Go语言通过名字来区分public和private,首字母大写是public,首字母小写是private;每个目录就是一个包;
方法接收者解释

//1、定义函数类型
//2、为已存在的数据类型起别名
type Int int //1、预处理 2、编译 3、汇编 4、链接

//方法:
//定义:1、func (方法接收者)方法名(参数列表)返回值类型
//     2、方法接收者也可以传递参数
//     3、调用时找的是方法接收者的类型,而不是名字(方法可以同名)
func (a Int)add(b Int)  Int{ // 可以理解为:把a的对象绑定add方法;add属于a类型,拥有a的所有方法,类似于其他语言中的this
	return a+b
}

func main()  {
	var a Int=10 // 根据数据类型,绑定方法
	value:=a.add(20) // a可以理解为对象,调用:对象.方法名
	fmt.Println(value)
}

2、方法中注意事项

  ①方法名可以和函数名重名;
  ②跨文件调用时,一定要进行初始化;
  ③方法名也可以重名,但是接收者特性必须不相等;

3、接口:type 接口名 interface{方法列表}

// 1、接口
// 格式:type 接口名 interfa{方法列表}
// 方法名 (参数列表) (返回值列表)
// 作用:定义规则,实现规则
type Humaner interface {
	// 2、接口中的方法必须有具体的实现
	SayHi()
}

type stu struct {
	score int
}

func (s stu)SayHi(){
	fmt.Println(s)
}

// 多态
// 多态将接口类型作为函数参数
func SayHi(p Humaner)  {
	p.SayHi()
}

func main()  {
	var s1 stu = stu{100}

	//3、定义接口类型
	//根据接口实现对应的方法
	var s2 Humaner

	s2=&s1
	s2.SayHi()

	// 多态的使用
	var s3 stu = stu{100}
	SayHi(s3)
}

4、接口的继承

  ①超集继承子集;
  ②将超级转成子集 反过来不允许;

5、空接口(可以存储任意类型)

//空接口  可以接收任意类型数据(万能指针)
var i interface{}
//空接口切片
var i []interface{}

类型断言:
注:只有接口才能做类型断言

arr:=make([]interface{},4)
arr[0]=123
arr[1]=3.1415
arr[2]="hello"
arr[3]=[]int{1,2,3}

for _,v:=range arr{

	if data,ok:=v.(int); ok{
		fmt.Println("整型数据:",data)
	}else if data,ok:=v.(float64); ok{
		fmt.Println("浮点型数据:",data)
	}else if data,ok:=v.(string); ok{
		fmt.Println("字符串数据:",data)
	}else if data,ok:=v.([]int); ok {
		fmt.Println("切片数据:",data)
	}

}

6、面向对象案例

package main

import "fmt"

type FuLei struct {
	num1 int
	num2 int
}

type JiaFaZiLei struct {
	FuLei
}

type JianFaZiLei struct {
	FuLei
}

type JieKou interface {
	JiSuan()int
}

func DouTai(jiekou JieKou)(value int)  {
	value=jiekou.JiSuan()
	return
}

func (jiafa *JiaFaZiLei)JiSuan()  int{
	return jiafa.num1 + jiafa.num2
}

func (jianfa *JianFaZiLei)JiSuan()  int{
	return jianfa.num1 - jianfa.num2
}

type GongChang struct {
	
}

func (gongchang *GongChang)JiSuanJieGuo(num1 int, num2 int, yunsuanfu string)(value int)  {

	// 创建接口变量
	var jiekou JieKou
	switch yunsuanfu {
	case "+":
		var jiafa JiaFaZiLei = JiaFaZiLei{FuLei{num1, num2}}
		//value=jiafa.JiSuan()
		jiekou=&jiafa
	case "-":
		var jianfa JianFaZiLei  = JianFaZiLei{FuLei{num1,num2}}
		//value=jianfa.JiSuan()
		jiekou=&jianfa
	}
	value=DouTai(jiekou)
	return
}

func main()  {
	var gongchang GongChang
	value:=gongchang.JiSuanJieGuo(10,20,"-")
	fmt.Println(value)
}

五、错误异常

1、error

func test(a int,b int)(value int, err error)  {
	// 0 不能作为除数
	if b==0 {
		err=errors.New("0不能作为除数")
		return
	}else{
		value=a/b
		return
	}
}
func main()  {
	value,err:=test(10,0)
	// err不等于空表示有错误信息
	if err != nil{
		fmt.Println(err)
	}else {
		fmt.Println(value)
	}
}

2、painc

1、直接调用;
2、可以在程序中直接调用panic 调用后程序会终止执行;
3、在开发过程中不建议直接调用;
panic("hello world2")

3、defer

1defer 函数调用并没有直接使用,而是先加载到栈区内存中,在函数结束时进行调用,重后往前执行;
defer fmt.Println("IT培训")
2、函数中有返回值不能使用defer调用;
defer test5(10, 20)

4、recover

func demo(i int){

	//recover()使用一定要在错误出现之前 进行拦截
	// 通过匿名函数和recover()进行错误拦截
	// 注意:recover只有在defer调用的函数中有效
	defer func(){
		//recover可以从panic 异常中重新获取控制权
		//recover()//返回值为接口类型

		//fmt.Println(recover())
		err:=recover()
		if err!=nil{
			fmt.Println("demo函数异常:19-33行",err)
		}
	}()
	fmt.Println("1")
	//空指针引用
	var p *int
	*p=123//err  //recover作用范围,出现错误该函数不会再往下执行

	fmt.Println("2")
	//数组元素个数为10个  len(arr)  数组下标是从0-9
	var arr [10]int


	//如果传递超出数组下标值 导致数组下标越界
	arr[i]=100//panic 系统处理  导致程序崩溃

	fmt.Println(arr)
}
func demo1(){
	fmt.Println("hello world")
}
func main() {
	demo(11)
	demo1()
}

六、文件处理

1、文件的创建

func main()  {
	// os.Create 同名文件会覆盖
	fp,err:=os.Create("D:/a.txt")

	// 判断文件是否创建成功
	if err != nil{
		fmt.Println("文件创建失败")
		return
	}

	//关闭文件,一般延迟调用
	//文件不关闭常出现的问题
	//1、占用内存和缓冲区
	//2、文件打开个数上限是65535个
	defer fp.Close()

	fmt.Println("文件创建成功")
}

2、打开文件

//打开文件
//openfile不能创建新文件,只能打开文件
//os.openfile(文件名路径,打开模式,打开权限)
// 读写O_RDWR,追加O_APPEND
// 权限:6是读写,7读写执行
fp,err:=os.OpenFile("D:/a.txt",os.O_RDWR,6)

if err !=nil{
	fmt.Println("打开文件失败")
	return
}

defer fp.Close()

3、文件的写入

//通过字符串获取字符切片
b:=[]byte("你瞅啥")

//获取文件字符个数
n,_:=fp.Seek(0,io.SeekEnd)// (从末尾开始的偏移量,从哪开始偏移)
n,_:=fp.Seek(3,io.SeekStart)
//负数是向左偏移 正数是向右偏移
n,_:=fp.Seek(-3,io.SeekEnd)
fmt.Println(n)

// 写入文件
//当使用writeAt 进行指定位置插入数据时会依次覆盖源内容
// WriteAt(写入内容, 偏移量)
fp.WriteAt(b,0)

4、文件的读取

// 文件按行读取

//创建切片缓冲区
r:=bufio.NewReader(fp)
//读取一行内容
b,_:=r.ReadBytes('\n')
//打印切片中字符的ASCII值
fmt.Println(b)
//将切片转成string打印  汉字
fmt.Print(string(b))
//如果在文件截取中  没有标志位(分隔符)到文件末尾自动停止  EOF -1 文件结束标志
b,_=r.ReadBytes('\n')

fmt.Println(string(b))

1)Read读取

func main(){
	fp,err:=os.Open("D:/源文件.txt")
	if err!=nil{
		fmt.Println("文件打开失败")
		return
	}
	defer fp.Close()
	b:=make([]byte,20)


	for{
		//读取文件返回值为 个数和错误信息
		n,err:=fp.Read(b)
		//io.EOF 表示文件的结尾 值为-1
		//eof  end of file
		if err == io.EOF{
			break
		}
		//if err !=nil{
		//	//io.EOF 表示文件的结尾 值为-1
		//	//eof  end of file
		//	if err == io.EOF{
		//		break
		//	}
		//}
		fmt.Print(string(b[:n]))
	}
}

2)缓冲区读取

func main(){
	fp,err:=os.Open("D:/源文件.txt")
	if err!=nil{
		fmt.Println("文件打开失败")
		return
	}
	defer fp.Close()

	//行读取
	r:=bufio.NewReader(fp)


	for{
		b,err:=r.ReadBytes('\n')

		fmt.Print(string(b))
		if  err == io.EOF{
			break
		}
	}
}

5、文件操作案例:拷贝

func main()  {
	// 文件拷贝 从一个文件中拷贝数据到新的文件中
	fp1,err1:=os.Open("C:\\Users\\zhtfi\\Desktop\\go临时存放\\01_Go语言基础第10天(异常处理和文件读写)\\03视频\\12文件操作案例.mp4")
	fp2,err2:=os.Create("D:/test.mp4")

	if err1 != nil || err2 != nil{
		fmt.Println("拷贝失败")
		return
	}

	defer fp1.Close()
	defer fp2.Close()

	b:=make([]byte,1024*1024*100)

	for{
		// 块读取
		n,err:=fp1.Read(b)
		if err==io.EOF{
			break
		}
		fp2.Write(b[:n])
	}
	fmt.Println("拷贝完成")

}

七、go程(groutine)

1、定义

在函数或方法前加go(进程线程概念

2、特性

  ①主程(main函数)退出后,子程退出;
  ②通过通信来共享内存,而不是共享内存来通信;

3、出让go程:gosched

runtime.Gosched() 用于让出CPU时间片,让出当前goroutine的执行权限,调度安排其它等待的任务运行,并在下次再获得cpu时间轮片的时候,从该出让cpu的位置恢复执行。
Demo:
func main()  {
	go func() {
		for {
			fmt.Println("--goroutine--")
		}
	}()

	for {
		runtime.Gosched() // 出让当前的cpu
		fmt.Println("this is main test")
	}
}

4、结束go程:goexit

定义:结束调用该函数的当前go程,runtime.Goexit() 之前注册的defer都生效。

5、协程等待

package main

import (
	"fmt"
	"sync"
	"time"
)

/**
 * @description: 协程等待,协程执行完成之后,退出程序
 * 注:正常程序,是一个线程,线程执行完毕,程序退出
 * @param {*}
 * @return {*}
 */
// 1
var wg sync.WaitGroup

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() 方法打印", i)
		time.Sleep(time.Millisecond * 100)
	}
	wg.Done() // 3、协程计数器减一
}
func test1() {
	for i := 0; i < 10; i++ {
		fmt.Println("test1() 方法打印", i)
		time.Sleep(time.Millisecond * 100)
	}
	wg.Done()
}
func main() {
	wg.Add(1) // 2、携程计数器加一
	go test() // 开启一个协程

	wg.Add(1)
	go test1()

	for i := 0; i < 10; i++ {
		fmt.Println("main() 方法打印", i)
		time.Sleep(time.Microsecond * 10)
	}
	wg.Wait() // 4、等待程序执行完毕
	fmt.Println("程序执行完毕")

}


6、设置CPU核数:GOMAXPROCS

Demo:
package main

import (
	"fmt"
	"runtime"
)

/**
 * @description: 设置cpu的数量,1.5 之前只能使用一个cpu
 * @param {*}
 * @return {*}
 */
func main() {
	// 获取当前计算机上cpu个数
	cpuNum := runtime.NumCPU()
	fmt.Println("该电脑上的CPU的个数为:", cpuNum)

	// 可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("OK")

}

7、channel

  ①定义:channel是一个数据类型,主要解决go程同步问题以及go程之间数据共享(数据传递)的问题;可以把channel看成是管道(水管),先进 先出;channel是一种类型,一种引用类型
  ②语法

channel的定义:
	声明:var 变量 chan 元素类型
	创建:make(chan 在channel中传递的数据类型, 容量)  容量=0,表示无缓冲channel;容量>0,有缓冲     			channel
	
channel有两个端:
    一端:写端(传入端)  chan <-
    另一端:读端(传出数据)  <- chan
    读端和写端必须同时满足条件,才能在chan上进行数据流动,否则阻塞。

Demo:
package main

import "fmt"

func main() {
	a := make(chan int, 10)
	a <- 1
	a <- 2
	b := a
	b <- 11
	<-a
	<-a
	fmt.Println(<-a) // 因为是引用传值,所以为 11
}

  ③无缓冲channel(同步通信)

通道容量为0len=0。不能存储数据。
channel应用于两个go程中。一个读,一个写。
具备同步的能力,读、写同步。

  ④有缓冲channel(异步通信)

通道容量为非0len(ch):channel中剩余未读取数据个数。cap(ch):通道容量。
channel应用于两个go程中,一个读,一个写。
缓冲区可以进行数据存储。存储至容量上限,阻塞。具备异步能力,不需要同时操作channel缓冲区。

  ⑤死锁、拥堵

当超过管道的容量时即为死锁或拥堵;

  ⑥channel 关闭、循环遍历

确定不再发送、接收数据,关闭channel,使用close(ch)关闭channe。
读端可以判断channel是否关闭:
    if num,ok:=<-ch; ok ==true{
    如果读端已经关闭,ok -->false.num无数据;
    如果读端没有关闭, ok -->true.num保存读到的数据;
总结:
    1、数据不发送完,不应该关闭;
    2、已经关闭的channel,不能再向其写数据;
    3、写端已经关闭channel,还可以从中读取数据,读完之后再读:int0string为空;(无缓冲,读到0;有缓冲,先读数据,读完后读到0)

Demo 1package main

import "fmt"

func main() {
	ch1 := make(chan int, 10)
	for i := 1; i <= 10; i++ {
		ch1 <- i
	}
	close(ch1) // 关闭管道
	// 通过for 循环可以不用关闭管道
	// for range循环遍历管道的值,需要关闭管道,注意:管道没有key
	for val := range ch1 {
		fmt.Println(val)
	}
}

  ⑦单向channel

默认定义的是双向管道
创建:make(chan<- int, 容量)

单向写channel:
    var s chan <- int    s=make(chan <- int)
单向读channel:
    var s <- chan int    s=make(<-chan int)
转换:
1、双向channel可以转换为任意一种单向channel;
2、单向channel不能转换为双向的channel;

  ⑧生产者消费者模型

生产者:发送数据端;
消费者:接收数据端;
缓冲区:1、解耦(降低生产者和消费者之间耦合度);
       2、并发(生产者消费者数量不对等时,能保持正常通信)3、缓存(生产者和消费者处理速度不一致时,暂存数据);

  ⑨定时器Timer

1、常用的三种定时方法 :
	sleep		time.Sleep(time.Second)
	Timer.C		myTimer:=timer.NewTimer(time.Second*2)   //创建定时器,指定定时时长
            	nowTime:= <-myTimer.C   //定时满,系统自动写入系统自动写入系统时间
	Timer.After nowTime:= <-time.After(time.Second *2)  // 两秒后的时间
2、定时器的停止和重置:
	func main()  {
		myTimer := time.NewTimer(time.Second * 10)		// 创建定时器。
		myTimer.Reset(1 * time.Second)		// 重置定时时长为 1
		go func() {
			for {
				<- myTimer.C
				fmt.Println("子go程,定时完毕")
			}
		}()
	
		myTimer.Stop()		// 设置定时器停止
		for {
			;
		}
	}
3、周期定时:
	func main()  {
		quit:=make(chan bool) // 创建一个判断是否终止的channel
	
		fmt.Println("now:  ", time.Now())
		myticker:=time.NewTicker(time.Second) // 定义一个周期定时器
	
		i:=1
		go func() {
			for {
				nowTime:= <-myticker.C
				i++
				fmt.Println("nowTime: ", nowTime)
				if i==30 {
					quit <- true // 解除 主go程阻塞
					break // return // runtime.Goexit
				}
			}
		}()
	
		<-quit // 子go程 循环获取 <-myTicker.C 期间,一直阻塞
	}

  ⑩其它信道相关操作
检查信道是否已经关闭

v, ok := <- ch

8、select

作用:用来监听channel上的数据流动。读或写;
用法:参考switch case语句。case后面必须是IO操作,不可以任意写判别表达式;不用关闭channel;
应用场景:从多个通道接收数据,减少阻塞;
注意事项:
1、监听case中,没有满足监听条件,阻塞。
2、监听case中,有多个满足监听条件,任选一个执行。
3、可以使用default来处理所有case都不满足监听条件的状况。通常不写default(会产生忙轮训)。
4、select自身不带循环机制,需借助外层for来循环监听。
5、break只能跳出select,类似switch中的用法。
6、select 写default,写了产生忙轮询,浪费资源;通常不写default,阻塞的程序会挂起,释放资源主动出让cpu,提高效率;

Demo:
package main

import (
	"fmt"
	"time"
)

func main() {
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}

	stringChannel := make(chan string, 20)
	for i := 0; i < 20; i++ {
		stringChannel <- "Hello" + fmt.Sprintf("%d", i)
	}

	for {
		select {
		case v := <-intChan:
			fmt.Printf("从 intChannel 读取的数据 %d\n", v)
			time.Sleep(time.Millisecond * 50)
		case v := <-stringChannel:
			fmt.Printf("从 stringChannel 读取的数据 %v\n", v)
			time.Sleep(time.Millisecond * 50)
		default:
			fmt.Printf("数据读取完毕")
			return
		}
	}
}

select 超时处理:

func main()  {
	ch := make(chan int)
	quit := make(chan bool)
	go func() {			// 子go 程获取数据
		for {
			select {
			case num := <-ch:
				fmt.Println("num = ", num)
			case <-time.After(3 * time.Second):
				quit <- true
				goto lable // 以下不执行,跳到指定的标签位置,标签名字是自定义的
				// return
				// runtime.Goexit()
			}
		}
	lable:
	}()
	for i:=0; i<2; i++ {
		ch <- i
		time.Sleep(time.Second * 2)
	}
	<-quit 			// 主go程,阻塞等待 子go程通知,退出
	fmt.Println("finish!!!")
}

9、锁的条件变化

1)死锁

Go语言中死锁在编译的时候会告诉你
常见的死锁案例:

// 1、单go程自己死锁
// channel应该在至少2个以上的go程中进行通信。否则死锁
func main01()  {
	ch:=make(chan int)
	ch <- 789 // 写死锁,程序不会往下进行
	num:=<-ch
	fmt.Println("num = ", num)
}

// 2、go程间channel访问顺序导致死锁
// 使用channel一端读(写),要保证另一端写(读)操作,同时有机会执行
func main02()  {
	ch:=make(chan int)
	num:=<-ch // 读死锁
	fmt.Println("num  = ",num)

	go func() {
		ch <- 789
	}()
}

// 3、多go程,多channel交叉死锁
func main()  {
	ch1:=make(chan int)
	ch2:=make(chan int)
	go func() {
		for  {
			select {
			case num:=<-ch1:
				ch2<-num
			}
		}
	}()
	for {
		select {
		case num:=<-ch2:
			ch1<-num
		}
	}
}
// 4、读写锁与channel产生死锁,隐性锁,不会报错
2)互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,通常在访问共享数据之前的添加锁;说白了就是在并发编程中让程序没有竞争;
互斥锁案例:

// 查看编译过程有没有问题
go build/run -race main.go

// 1、使用 “锁” 完成同步 —— 互斥锁
var mutex sync.Mutex		// 创建一个互斥量, 新建的互斥锁状态为 0. 未加锁。 锁只有一把。

func printer(str string)  {
	mutex.Lock()			// 2、访问共享数据之前,加锁
	for _, ch := range str {
		fmt.Printf("%c", ch)
		time.Sleep(time.Millisecond * 300)
	}
	mutex.Unlock()			//3、 共享数据访问结束,解锁
}

func person1()  {				// 先
	printer("hello")
}

func person2()  {				// 后
	printer("world")
}

func main()  {
	go person1()
	go person2()
	for {
		;
	}
}

3)读写锁

读时共享,写时独占。写锁优先级比读锁高。当一个gotoutine进行写操作的时候,其它gorutine既不能进行读操作,也不能进行写操作。

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var rwm sync.RWMutex

func write() {
	rwm.Lock() // 写锁
	fmt.Println("执行写操作")
	time.Sleep(time.Second * 2)
	wg.Done()
	rwm.Unlock()
}

func read() {
	rwm.RLock() // 读锁
	fmt.Println("-------执行读操作")
	time.Sleep(time.Second * 2)
	wg.Done()
	rwm.RUnlock()
}

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
}

4)条件变量

条件变量不是锁,当经常要与锁结合。
条件变量常用的三个方法:Wait、Signal、Broadcast

使用流程:
1.  创建 条件变量: var cond    sync.Cond
2.  指定条件变量用的 锁:  cond.L = new(sync.Mutex)
3.  cond.L.Lock()	给公共区加锁(互斥量)
4.  判断是否到达 阻塞条件(缓冲区满/空)—— for 循环判断
for  len(ch) == cap(ch) {   cond.Wait() —— 1) 阻塞 2) 解锁 3) 加锁
5.  访问公共区 —— 读、写数据、打印 
6.  解锁条件变量用的 锁  cond.L.Unlock()
7.  唤醒阻塞在条件变量上的 对端。 signal()  Broadcast()

案例(注:wait使用for):

var cond sync.Cond			// 定义全局条件变量

func producer08(out chan<- int, idx int)  {
	for {
		// 先加锁
		cond.L.Lock()
		// 判断缓冲区是否满
		for len(out) == 5 {
			cond.Wait()			// 1. 2. 3.
		}
		num := rand.Intn(800)
		out <- num
		fmt.Printf("生产者%dth,生产:%d\n", idx, num)
		// 访问公共区结束,并且打印结束,解锁
		cond.L.Unlock()
		// 唤醒阻塞在条件变量上的 消费者
		cond.Signal()
		time.Sleep(time.Millisecond * 200)
	}
}

func consumer08(in <-chan int, idx int)  {
	for {
		// 先加锁
		cond.L.Lock()
		// 判断 缓冲区是否为空
		for len(in) == 0 {
			cond.Wait()
		}
		num := <-in
		fmt.Printf("-----消费者%dth,消费:%d\n",idx,  num)
		// 访问公共区结束后,解锁
		cond.L.Unlock()
		// 唤醒 阻塞在条件变量上的 生产者
		cond.Signal()
		time.Sleep(time.Millisecond * 200)
	}
}

func main()  {
	product := make(chan int, 5)
	rand.Seed(time.Now().UnixNano())

	quit := make(chan bool)

	// 指定条件变量 使用的锁
	cond.L = new(sync.Mutex)				// 互斥锁 初值 0 , 未加锁状态

	for i:=0; i<5; i++ {
		go producer08(product, i+1)			// 1 生产者
	}
	for i:=0; i<5; i++ {
		go consumer08(product, i+1)			// 3 个消费者
	}
/*	for {
		runtime.GC()
	}*/
	<-quit
}

八、网络通信

1、TCP通信

面向相连接的,可靠的数据包传输;消耗资源;

1)TCP单通信

  ①TCP-CS 服务器

TCP-CS服务器:
1.  创建监听socket  listener := net.Listen("TCP", "IP+port")	IP+port	—— 服务器自己的IP 和 port
2.  启动监听  conn := listener.Accept()  conn 用于 通信的 socket
3.  conn.Read()  
4.  处理使用 数据
5.  conn.Write()
6.  关闭  listener、conn

案例:

func main()  {
	// 指定服务器 通信协议、IP地址、port。 创建一个用于监听的 socket
	listener,err:=net.Listen("tcp", "127.0.0.1:8000")
	if err!=nil{
		fmt.Println("net.Listen err:",err)
		return
	}
	defer listener.Close()
	fmt.Println("服务端等待客户端建立连接...")

	// 阻塞监听客户端连接请求, 成功建立连接,返回用于通信的socket
	conn,err:=listener.Accept()
	if err!=nil{
		fmt.Println("listener.Accept err:",err)
		return
	}
	defer conn.Close()
	fmt.Println("服务器与客户端成功建立连接!!!")

	// 读取客户端发送的数据
	buf:=make([]byte,4096)
	n,err:=conn.Read(buf)
	if err!=nil{
		fmt.Println("conn.Read err:", err)
		return
	}
	conn.Write(buf[:n]) // 读多少写多少,原封不动
	fmt.Println("服务器读到数据:", string((buf[:n])))
}

  ②TCP-CS 客户端

TCP-CS客户端:
1.  conn, err := net.Dial("TCP", 服务器的IP+port)
2.  写数据给 服务器 conn.Write()
3.  读取服务器回发的 数据 conn.Read()
4.  conn.Close()

案例:

func main()  {
	// 指定 服务器 IP + port 创建 通信套接字。
	conn,err:=net.Dial("tcp", "127.0.0.1:8000")
	if err!=nil{
		fmt.Println("net.Dial err: ", err)
		return
	}
	defer conn.Close()

	// 主动写数据给服务器
	conn.Write([]byte("Are you Ready?"))

	buf:=make([]byte, 4096)
	// 接收服务器回发的数据
	n,err:=conn.Read(buf)
	if err!=nil{
		fmt.Println("conn.Read err:", err)
		return
	}
	fmt.Println("服务器回发:", string(buf[:n]))
}
2)TCP并发通信

  ① TCP-CS并发服务器

TCP-CS并发服务器:
1.  创建 监听套接字 listener := net.Listen("tcp", 服务器的IP+port)		// tcp 不能大写
2.  defer listener.Close()
3.  for 循环 阻塞监听 客户端连接事件 	conn := listener.Accept()
4. 创建 go程 对应每一个 客户端进行数据通信	go HandlerConnet()
5. 实现 HandlerConnet(conn net.Conn) 
1) defer conn.Close()
2) 获取成功连接的客户端 Addr 		conn.RemoteAddr()
3) for 循环 读取 客户端发送数据		conn.Read(buf)
4) 处理数据 小 —— 大	strings.ToUpper()
5)回写转化后的数据		conn.Write(buf[:n]) 

案例:

func main()  {
	// 1、监听套接字
	listen,err:=net.Listen("tcp", "127.0.0.1:8001")
	if err!=nil{
		fmt.Println("net.Listen err: ", err)
		return
	}
	defer listen.Close()

	// 2、循环阻塞监听客户端连接请求
	for  {
		fmt.Println("服务器等待客户端连接...")
		conn,err:=listen.Accept()
		if err!=nil{
			fmt.Println("listen.Accept err: ", err)
			return
		}
		// 具体完成服务端和客户端的数据通信
		go HandllerConnect(conn)
	}
}

func HandllerConnect(conn net.Conn)  {

	defer conn.Close()

	// 获取连接的客户端 Addr
	addr:=conn.RemoteAddr()
	fmt.Println(addr, "客户端连接成功!")

	// 循环读取客户端发送的数据
	buf:=make([]byte, 4096)
	for {
		n,err:=conn.Read(buf)
		if "exit\n"==string(buf[:n]) || "exit\r\n"==string(buf[:n]){
			fmt.Println("服务器接收客户端推出请求的,服务关闭")
			return
		}
		if n==0{
			fmt.Println("服务器检测到客户端已关闭,断开连接!!!")
			return
		}
		if err!=nil{
			fmt.Println("conn.Read err:", err)
			return
		}
		fmt.Println("服务器读到数据: ", string(buf[:n])) // 使用数据

		// 小写转大写,会发给客户端
		conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
	}
}

  ② TCP-CS并发客户端

TCP-CS并发客户端:	
1. 匿名 go 程 , 获取 键盘输入, 写给服务器
2. for 循环读取服务器回发数据
发送数据时,默认在结尾自带‘ \r\n ’

案例:

func main()  {
	// 主动发起连接请求
	conn,err:=net.Dial("tcp", "127.0.0.1:8001")
	if err!=nil{
		fmt.Println("net.Dial err: ", err)
		return
	}
	defer conn.Close()

	// 获取用户键盘输入(stdin),将输入数据发送给服务器
	go func() {
		str:=make([]byte, 4096)
		for {
			n,err:=os.Stdin.Read(str)
			if err!=nil{
				fmt.Println("os.Stdin.Read err: ", err)
				continue
			}
			// 写给服务器,读多少,写多少!
			conn.Write(str[:n])
		}
	}()

	// 回显服务器回发的大写数据
	buf:=make([]byte, 4096)
	for {
		n,err:=conn.Read(buf)
		if n==0{
			fmt.Println("检查服务器关闭,客户端也关闭")
			return
		}
		if err!=nil{
			fmt.Println("conn.Read err:", err)
			return
		}
		fmt.Println("客户端读到服务器回发:", string(buf[:n]))
	}

}

2、UDP通信

1)UDP 单通信

  ①UPCP-CS服务器

UCP-CS服务器:
1.  创建 server端地址结构(IP + port) net.ResolveUDPAddr()
2.  创建用于通信的socket, 绑定地址结构	udpConn = net.ListenUDP(“udp”, server端地址结构)
3.  defer udpConn.Close()
4.  读取客户端发送数据	ReadFromUDP(buf)   返回: n, cltAddr(客户端的IP+port) , err
5.  写数据给 客户端		WriteToUDP("待写数据",cltAddr)

案例:

func main()  {
	// 组织一个 udp地址结构,指定服务器的IP+port
	srvAddr,err:=net.ResolveUDPAddr("udp","127.0.0.1:8003")
	if err != nil {
		fmt.Println("ResolveUDPAddr err:", err)
		return
	}
	fmt.Println("udp 服务器地址结构,创建完程!!!")

	// 创建用的通信的 socket
	udpConn,err:=net.ListenUDP("udp", srvAddr)
	if err != nil {
		fmt.Println("ListenUDP err:", err)
		return
	}
	defer udpConn.Close()
	fmt.Println("udp 服务器通信socket创建完成!!!")

	// 读取客户端发送的数据
	buf:=make([]byte, 4096)

	// 返回3个值,分别是:读取到的字节数、客户端地址,error
	n, cltAddr, err:=udpConn.ReadFromUDP(buf)
	if err != nil {
		fmt.Println("ReadFromUDP err:", err)
		return
	}
	// 模拟处理数据
	fmt.Printf("服务器读到 %v 的数据:%s\n", cltAddr, string(buf[:n]))

	// 提取当前时间
	daytime:=time.Now().String()

	// 回写数据给客户端
	_,err=udpConn.WriteToUDP([]byte(daytime), cltAddr)
	if err != nil {
		fmt.Println("WriteToUDP err:", err)
		return
	}

}

  ② UPCP-CS客户端

TCP-CS客户端:
参考 TCP 客户端。
net.Dial("udp", server 的IP+port)
2)UDP 并发通信

3、TCP与UDP比较

Tcp
优点:稳定、安全、有序;
缺点:效率低、开销大。开发复杂度高;
使用场景:对数据传输安全性、稳定性要求较高的场合, 网络文件传输。下载、上传;
Udp
优点:效率高、开销小。开发复杂度低;
缺点:稳定性差、安全低、无序;
使用场景:对数据实时传输要求较高的场合。视频直播、在线电话会议。游戏;

4、网络文件传输

1)获取命令行参数:os.Args
命令行参数: 在main函数启动时,向整个程序传参。
语法: go run xxx.go  argv1 argv2  argv3  argv4

xxx.go:0 个参数。
argv1 :第 1 个参数。
argv2 :第 2个参数。	
argv3 :第 3 个参数。
argv4 :第 4 个参数。

使用: list := os.Args
参数3 = list[3]
2)获取文件属性
获取文件属性:
fileInfo:os.stat(文件访问绝对路径)
fileInfo 接口,两个接口。
Name() 获取文件名。
Size() 获取文件大小。

Demo:
func main()  {
	list:=os.Args // 获取命令行参数

	if len(list)!=2{
		fmt.Println("格式为:go run xxx.go 文件名")
		return
	}

	// 提取文件名
	path:=list[1]

	// 获取文件属性
	fileInfo,err:=os.Stat(path)
	if err!=nil{
		fmt.Println("os.Stat err:", err)
		return
	}
	fmt.Println("文件名:", fileInfo.Name())
	fmt.Println("文件大小:", fileInfo.Size())
}

3)文件传输-发送端
文件传输——发送端(客户端):
1. 提示用户使用命令行参数输入文件名。接收文件名 filepath(含访问路径)
2. 使用 os.Stat()获取文件属性,得到纯文件名 fileName(去除访问路径)
3. 主动发起连接服务器请求,结束时关闭连接。
4. 发送文件名到接收端 conn.Write()
5. 读取接收端回发的确认数据 conn.Read()
6. 判断是否为“ok”。如果是,封装函数 SendFile() 发送文件内容。传参 filePath 和 conn
7. 只读 Open 文件, 结束时Close文件
8. 循环读本地文件,读到 EOF,读取完毕。
9. 将读到的内容原封不动 conn.Write 给接收端(服务器)

// 文件发送端——客户端
func main()  {
	list:=os.Args

	if len(list)!=2{
		fmt.Println("格式为:go run xxx.go 文件绝对路径")
		return
	}

	// 提取文件的绝对路径
	filePath:=list[1]

	// 提取文件名
	fileInfo, err:=os.Stat(filePath)
	if err!=nil{
		fmt.Println("os.Stat err:", err)
		return
	}
	fileName:=fileInfo.Name()

	// 主动发送连接请求
	conn,err:=net.Dial("tcp", "127.0.0.1:8005")
	if err != nil {
		fmt.Println("net.Dial err:", err)
		return
	}
	defer conn.Close()

	// 发送文件名给接收端
	_,err=conn.Write([]byte(fileName))
	if err != nil {
		fmt.Println("conn.Write err:", err)
		return
	}
	// 读取服务器回发的 OK
	buf:=make([]byte,16)
	n,err:=conn.Read(buf)
	if err != nil {
		fmt.Println("conn.Read err:", err)
		return
	}

	if "ok"==string(buf[:n]){
		// 写文件内容给服务器——借助conn
		sendFile(conn, filePath)
	}
}

func sendFile(conn net.Conn, filePath string)  {
	// 只读打开文件
	f,err:=os.Open(filePath)
	if err != nil {
		fmt.Println("os.Open err:", err)
		return
	}
	defer f.Close()
	
	// 从本文件中,读数据,写给网络接收端。读多少,写多少
	buf:=make([]byte, 4096)
	for  {
		// 从本地读
		n, err := f.Read(buf)
		if err != nil {
			if err == io.EOF {
				fmt.Println("发送文件完成。")
			} else {
				fmt.Println("os.Open err:", err)
			}
			return
		}
		// 发送到网络中 写到网络socket中
		_, err = conn.Write(buf[:n])
		if err != nil {
			fmt.Println("conn.Write err:", err)
			return
		}
	}
} 
4)文件传输-接收端
文件传输——接收端(服务器):
1. 创建监听 listener,程序结束时关闭。
2. 阻塞等待客户端连接 conn,程序结束时关闭conn。
3. 读取客户端发送文件名。保存 fileName。
4. 回发“ok”。
5. 封装函数 RecvFile 接收客户端发送的文件内容。传参 fileName 和 conn
6. 按文件名 Create 文件,结束时 Close
7. 循环 Read 发送端网络文件内容,当读到 0 说明文件读取完毕。
8. 将读到的内容原封不动Write到创建的文件中

// 文件接收端——服务端
func main()  {
	// 创建监听的Socket
	listener,err:=net.Listen("tcp", "127.0.0.1:8005")
	if err != nil {
		fmt.Println(" net.Listen err:", err)
		return
	}
	defer listener.Close()

	// 阻塞监听
	conn,err:=listener.Accept()
	if err != nil {
		fmt.Println(" listener.Accept() err:", err)
		return
	}
	defer conn.Close()

	// 获取文件名,保存
	buf:=make([]byte, 4096)
	n,err:=conn.Read(buf)
	if err != nil {
		fmt.Println(" conn.Read err:", err)
		return
	}
	fileName:=string(buf[:n])

	// 回写 ok 给发送端
	conn.Write([]byte("ok"))

	// 获取文件内容
	recvFile(conn, fileName)
}

func recvFile(conn net.Conn, fileName string)  {
	// 按照文件名创建新文件
	f, err := os.Create(fileName)
	if err != nil {
		fmt.Println("os.Create err:", err)
		return
	}
	defer f.Close()

	// 从 网络中读数据,写入本地文件
	buf := make([]byte, 4096)
	for {
		n,_ := conn.Read(buf)
		if n == 0 {
			fmt.Println("接收文件完成。")
			return
		}
		// 写入本地文件,读多少,写多少。
		f.Write(buf[:n])
	}
}

九、面试

golang面试题:reflect(反射包)如何获取字段tag​?为什么json包不能导出私有变量的tag? 文章末尾

Golang 面试题搜集

Golang精编面试题100 面试题内容

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值