一、开发文档
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)类型转换
1、int32和int64都是整形,只有类型匹配的数据才能进行计算;
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()
# 引用数据类型(map、切片、指针),在声明的时候需要初始化操作,分配内存空间
make 创建存储空间,适用于map、切片、channel的内存创建,返回的类型是本身,而非它们的指针类型;
Demo:
var user = make(map[string]string)
var user = make([]int, 4, 4) // 第一个参数是类型,第二个参数是分配的空间,第三个参数是预留分配空间
注:selice可以不用make初始化,map和chan必须使用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
1、defer 函数调用并没有直接使用,而是先加载到栈区内存中,在函数结束时进行调用,重后往前执行;
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(同步通信)
通道容量为0,len=0。不能存储数据。
channel应用于两个go程中。一个读,一个写。
具备同步的能力,读、写同步。
④有缓冲channel(异步通信)
通道容量为非0。len(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,还可以从中读取数据,读完之后再读:int为0、string为空;(无缓冲,读到0;有缓冲,先读数据,读完后读到0)
Demo 1:
package 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])
}
}