学习内容:
go mod依赖管理
- 初始化项目:go mod init 模块名
- 在pkg中下载指定的当前模块,依赖一律不下:go mod download 下载路径
- 根据需要的依赖,将引用的依赖缺少的补齐,多余的删除:go mod tidy
- 编辑go.mod文件:go mod edit -选项=内容
- 有vendor文件夹会在该文件夹下找依赖
go mod vendor会将pkg/mod下的依赖全部复制到vendor文件夹下 - 验证所依赖的mod是否发生改变:go mod verify
- 找出现问题的原因:go mod why
go.mod文件
//模块名,根目录
module Ajinjie
//golang sdk 版本
go 1.20
//指定项目第三方依赖
require{
//依赖 版本号
// dependency latest
}
//排除第三方依赖
exclude{
// dependency latest
}
//替换第三方依赖 路径或版本号
replace{
// source latest => target latest
}
//撤回有问题的版本
retract(
//版本1.0.0被标记为有问题,
//撤回该版本,寻找其他版本
v1.0.0
)
常量
- 使用const修饰
- 在定义的时候,必须需初始化
- 常量不能修改
- 只能修饰bool,数值类型(int,float系列),string类型
- 通过首字母的大小写来控制常量的访问范围
var num int
//变量类型可写可不写
// const tax int
// missing init expr for tax
const tax int = 66
fmt.Println(num," ",tax)
num = 99
// tax = 88
// cannot assign to tax (constant 66 of type int)
fmt.Println(num," ",tax)
const b = 9/3
fmt.Println(b)
num = 9
// const c = num/3
// num / 3 (value of type int) is not constant
//因为编译器认为变量是有可能改变的
// fmt.Println(c)
//同样报错
// const c = getVal()
/* 0 66
99 66
3 */
const(
a = 1
d = 2
)
fmt.Println(a," ",d)
//表示给i赋值为0,i1在i的基础上+1,i2在i1的基础上+1
const(
i = iota
i1
i2
)
fmt.Println(i," ",i1," ",i2)
/* 1 2
0 1 2 */
方法
go的方法是作用在指定数据类型上的(和数据类型相互绑定),所以自定义类型,都可以有方法
需要声明方法,比如Person结构体除字段外,还有行为用方法完成
方法首字母小写只能本包访问,首字母大写可以所有包访问
- 定义和调用
type Teacher struct{
Name string
}
type Student struct{
Name string
}
//将该方法和Teacher类型进行绑定
func (t Teacher) teach(){
t.Name = "周六"
//因为是变量时值类型,所以是值拷贝
//在方法内对数据进行修改,不会影响到方法外数据
fmt.Println(t.Name,"老师正在教课")
// 周六 老师正在教课
}
func main() {
var t Teacher
t.Name = "王五"
//t在调用方法时会传到方法
t.teach()
fmt.Println(t.Name,"老师正在教课")
// 王五 老师正在教课
//该方法只能通过Teacher类型进行调用
/* s.teach undefined (type Student
has no field or method teach) */
/* var s Student
s.Name = "麻子"
s.teach() */
// undefined: teach
// teach()
}
- 通过自定义类型的指针调用指定方法,修改变量内的数据
type Teacher struct{
Name string
}
//接收变量的地址值
func (t *Teacher) change() {
//两种方式等价
//t.Name = "周六"
(*t).Name = "周六"
/* fmt.Println(t)
&{周六} */
fmt.Printf("t的地址:%p",t)
// t的地址:0xc000042270
}
func main() {
var t *Teacher=&Teacher{}
(*t).Name = "王五"
fmt.Print((*t).Name,"老师被调课为")
t.change()
fmt.Println((*t).Name,"老师")
// 王五老师被调课为周六 老师
var t1 Teacher
t1.Name = "张三"
(&t1).change()
//编译器底层会做出优化
//自动识别到需要传入的是指针类型,传入变量的地址值
t1.change()
fmt.Println(t1.Name)
// 周六
fmt.Printf("t的地址:%p",t)
// t的地址:0xc000042270
}
- 在方法内进行运算
func (te Teacher) add(num int) int{
res := 0
for i := 1; i <= num; i++ {
res += i
}
return res
}
func main() {
var t Teacher
t.Name = "王五"
//方法在调用的同时,会将调用变量的值同时做为实参传入方法的对应变量上
fmt.Println(t.Name,"进行计算为",t.add(99))
// 王五 进行计算为 4950
}
- 其他自定义类型
type myInt int
type myFloat float32
func (i myInt) print() {
fmt.Println("i=",i)
}
func (f *myFloat) change() {
/* invalid operation: f + 1 (mismatched
types *myFloat and untyped int) */
// f = f+1
*f = *f + 1
}
func main() {
var i myInt = 10
i.print()
// myFloat (type) is not an expression
// var f *myFloat = &myFloat
/* cannot use 3.2 (untyped float constant)
as *myFloat value in assignment */
var f myFloat = 3.2
//可以自动转换
f.change()
fmt.Println("f=",f)
// f= 4.2
}
- 一个类型实现String()方法,fmt.Println默认会调用这个变量的String()进行输出
type Student struct{
Name string
Age int
}
//实现String()方法
func (stu *Student) String() string {
str :=fmt.Sprintf("name=%v,age=%v",stu.Name,stu.Age)
return str
}
func main() {
stu :=Student{
Name : "张三",
Age : 20,
}
fmt.Println(stu)
//自动调用String方法
//没有实现会输出结构体的地址 &{张三 20}
fmt.Println(&stu)
/* {张三 20}
name=张三,age=20 */
}
- 方法和函数的区别
不管调用形式,真正决定值拷贝或地址拷贝,主要看方法和哪个类型绑定
type Teacher struct{
Name string
}
//函数接收者为值类型时,不能将指针类型的数据直接传递
//相反一样
func test(t Teacher) {
fmt.Println("test:",t.Name)
// test: 张三
}
func test2(t *Teacher) {
fmt.Println("test2:",t.Name)
// test2: 张三
}
//方法接收者为值类型时,可以直接用指针类型的变量调用方法
//相反一样
func (t Teacher)test1() {
t.Name = "王五"
fmt.Println("test1:",t.Name)
// test1: 王五
}
func (t *Teacher)test3() {
t.Name = "麻子"
fmt.Println("test3:",t.Name)
// test3: 麻子
}
func main() {
t := Teacher{
Name :"张三",
}
test(t)
// test: 张三
/* cannot use &t (value of type *Teacher)
as Teacher value in argument to test */
// test(&t)
/* cannot use t (variable of type Teacher)
as *Teacher value in argument to test2 */
// test2(t)
t2 := &Teacher{
Name :"张三",
}
test2(t2)
// test2: 张三
t1 := Teacher{"周六"}
t1.test1()
// test1: 王五
//底层优化,识别到是值类型后会自动进行值拷贝
(&t1).test1()
fmt.Println("main的test1:",t1.Name)
// main的test1: 周六
t1.test3()
fmt.Println("main的test3:",t1.Name)
// main的test3: 麻子
}
工场模式
解决go的结构体没有构造函数
在其他包创建实例时,当首字母大写可以直接导入包后创建,但当首字母小写时,就要通过工场模式解决
package model
type Student struct{
Name string
Score float64
}
//相当于私有
type student struct{
Name string
// Score float64
score float64
}
//用工场模式代理生成实例
func NewStudent(name string,score float64) *student{
return &student{
Name : name,
// Score : score,
score : score,
}
}
// main包中fmt.Println("姓名:",stu1.Name,",成绩:",stu1.score)报错
//当结构体中的属性名首字母小写,不可以在其他包直接访问
//解决
func (stu *student) GetScore() float64 {
return stu.score
}
package main
import (
"fmt"
"Ajinjie/day2/model"
/* package Ajinjie/day2/model is not in
GOROOT (G:\go\src\Ajinjie\day2\model) */
// 出现上述报错时
//回到项目目录下执行以下代码
//或者直接把项目拖到指定的位置(非常不建议)
/* 初始化模块 go mod init
下载依赖包 go mod tidy */
)
func main() {
var stu = model.Student{
Name : "张三",
Score : 66.6,
}
fmt.Println(stu)
// {张三 66.6}
stu1 := model.NewStudent("周六",99.9)
fmt.Println(*stu1)
// {周六 99.9}
/* stu1.score undefined (type *model.student
has no field or method score) */
// fmt.Println("姓名:",stu1.Name,",成绩:",stu1.score)
fmt.Println("姓名:",stu1.Name,",成绩:",stu1.GetScore())
// 姓名: 周六 ,成绩: 99.9
}
抽象
定义一个结构体时,将一类事物共有的属性和发那个发提取出来,形成以一个物理模型
type Account struct{
AccountNo string
Pwd string
Balance float64
}
func (a *Account) Deposite(money float64,pwd string) {
if pwd != a.Pwd{
fmt.Println("密码不正确")
return
}
if money <= 0{
fmt.Println("金额不正确")
return
}
a.Balance += money
fmt.Println("存款成功")
}
func (a *Account) WithDraw(money float64,pwd string) {
if pwd != a.Pwd{
fmt.Println("密码不正确")
return
}
if money <= 0 || money > a.Balance{
fmt.Println("金额不正确")
return
}
a.Balance -= money
fmt.Println("取款成功")
}
func (a *Account) Query(pwd string) {
if pwd != a.Pwd{
fmt.Println("密码不正确")
return
}
fmt.Println("余额:",a.Balance)
}
func main() {
a := Account{
AccountNo : "666666",
Pwd : "123456",
Balance : 99.9,
}
a.Deposite(900,a.Pwd)
a.WithDraw(333.3 ,a.Pwd)
a.Query(a.Pwd)
/* 存款成功
取款成功
余额: 666.5999999999999 */
}
接口
高内聚低耦合,保障方法的规范性
- 定义一组方法且没有方法体,不需要被实现,并且接口中不能包含任何变量。要使用某个自定义类型时,再根据具体情况写出这些方法(实现)
- 接口不需要显式实现(没有implement关键字),只需一个变量,含有接口类型中所有方法,实现接口是基于方法而不是接口名
//声明接口
type Usb interface{
//声明未实现方法
Start()
Stop()
}
type phone struct{
}
func (p phone) Start() {
fmt.Println("手机开始工作")
}
func (p phone) Stop() {
fmt.Println("手机停止工作")
}
type Cammer struct{
}
func (c Cammer) Start() {
fmt.Println("相机开始工作")
}
func (c Cammer) Stop() {
fmt.Println("相机停止工作")
}
type Computer struct{
}
//接收一个Usb接口类型变量
//实现接口,就是实现该接口声明的所有方法
func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
}
func main() {
var c Computer
var ca Cammer
var p phone
c.Working(ca)
c.Working(p)
/* 相机开始工作
相机停止工作
手机开始工作
手机停止工作 */
}
- 接口本身不能创建实例对象,可以指向一个实现该接口的自定义类型变量
type A interface {
Say()
}
type B struct{
Name string
}
func (b B) Say() {
fmt.Println("b say")
}
func main() {
var a A
// <nil>
fmt.Println(a)
var b B
a = b
a.Say()
// b say
}
- 接口中所有的方法都没有方法体
- go中,一个自定义类型需要将某接口所有方法实现,才是实现该接口
- 只要是自定义数据类型,就可以实现接口
type A interface {
Say()
}
type myInt int
func (m myInt) Say() {
fmt.Println("myInt say")
}
func main() {
var m myInt
var a1 A = m
a1.Say()
// myInt say
}
- 一个自定义类型可以实现多个接口
type A interface {
Say()
}
type B interface {
Hello()
}
type Monster struct {
}
func (m Monster) Say() {
fmt.Println("monster say")
}
func (m Monster) Hello() {
fmt.Println("monster hello")
}
func main() {
var m Monster
var a A = m
var b B = m
a.Say()
b.Hello()
/* monster say
monster hello */
}
- 一个接口可以继承多个接口,实现该接口,同时必须实现继承的接口,在继承时,不允许出现相同的方法名
type A interface{
test()
B
C
}
type B interface{
test1()
}
type C interface{
test2()
}
type Student struct{
}
func (stu Student) test() {
fmt.Println("test")
}
func (stu Student) test1() {
fmt.Println("test1")
}
/* cannot use stu (variable of type Student) as A value in variable
declaration: Student does not implement A (missing method test2) */
func (stu Student) test2() {
fmt.Println("test2")
}
func main() {
var stu Student
var a A = stu
a.test()
a.test1()
a.test2()
/* test
test1
test2 */
}
- 接口类型默认是一个指针(引用类型),没有对接口初始化就会输出nil
- 空接口interface{},没有任何方法,所有而类型都实现空接口,可以把任意变量赋值给空接口
var t interface{} = stu
fmt.Println(t)
// {}
var num int =66
t = num
fmt.Println(t)
// 66
- 当利用自定义类型的指针类型实现接口的所有方法,在调用时赋值给接口的是自定义类型,此时会报错
type B interface {
Hello()
}
type Monster struct {
}
//因为实现接口的类型不同
func (m *Monster) Hello() {
fmt.Println("monster hello")
}
/* func (m Monster) Hello() {
fmt.Println("monster hello")
} */
func main() {
var m Monster
/* cannot use m (variable of type Monster) as B value in variable
declaration: Monster does not implement B (method Hello has pointer receiver) */
// var b B = m
var b B = &m
b.Hello()
// monster hello
}
- 接口的实例化和使用
package main
import (
"fmt"
"sort"
"math/rand"
)
type Hero struct{
Name string
Age int
}
type HeroSlice []Hero
//实现interface接口
func (hs HeroSlice) Len() int {
return len(hs)
}
//Less排序方式
func (hs HeroSlice) Less(i,j int) bool {
return hs[i].Age < hs[j].Age
}
//交换
func (hs HeroSlice) Swap(i,j int) {
/* temp := hs[i]
hs[i] = hs[j]
hs[j] = temp */
hs[i], hs[j] = hs[j], hs[i]
}
func main() {
//定义数组或者切片
var slice = []int{0,-1,10,7,90}
//排序
//因为slice是引用类型,不需要再加&
sort.Ints(slice)
fmt.Println(slice)
// [-1 0 7 10 90]
//对结构体切片进行排序
var heroes HeroSlice
for i := 0; i < 10; i++ {
hero := Hero{
// rand随机数
Name : fmt.Sprintf("英雄 %d",rand.Intn(100)),
Age : rand.Intn(100),
}
heroes = append(heroes,hero)
}
for _, v := range heroes {
fmt.Println(v)
}
/* {英雄 83 69}
{英雄 84 82}
{英雄 65 55}
{英雄 99 8}
{英雄 41 57}
{英雄 41 91}
{英雄 28 37}
{英雄 80 47}
{英雄 5 56}
{英雄 73 13} */
sort.Sort(heroes)
fmt.Println("排序后")
for _, v := range heroes {
fmt.Println(v)
}
/* 排序后
{英雄 99 8}
{英雄 73 13}
{英雄 28 37}
{英雄 80 47}
{英雄 65 55}
{英雄 5 56}
{英雄 41 57}
{英雄 83 69}
{英雄 84 82}
{英雄 41 91} */
}
- 接口可以再不破坏继承关系的基础上对方法进行扩展
类型断言
由于接口是一般类型,要转成具体类型,就需要使用类型断言
- 使用,当类型不匹配时会报panic
type Point struct {
x int
y int
}
func main() {
var a interface{}
var point Point = Point{1,2}
a = point
var b Point
/* cannot use a (variable of type interface{})
as Point value in assignment: need type assertion */
// b = a
//断言,当a可以转换为Point类型时可以转换成功
b = a.(Point)
fmt.Println(b)
// {1 2}
c := a.(int)
// interface {} is main.Point, not int
fmt.Println(c)
}
- 检测类型断言
panic函数被调用时会返回一个空接口类型的结果值,返回值的具体内容时传递给panic函数的参数值的副本
var x interface{}
var b float32 = 1.1
x = b
if y,ok := x.(float64); ok {
fmt.Printf("y的类型:%T,值:%v \n",y,y)
}else{
fmt.Println("转换失败")
}
fmt.Println("继续执行")
/* 转换失败
继续执行 */
- 实例
type Usb interface{
Start()
Stop()
}
type phone struct{
Name string
}
func (p phone) Start() {
fmt.Println("手机开始工作")
}
func (p phone) Stop() {
fmt.Println("手机停止工作")
}
func (p phone) Call() {
fmt.Println("正在打电话")
}
type Cammer struct{
Name string
}
func (c Cammer) Start() {
fmt.Println("相机开始工作")
}
func (c Cammer) Stop() {
fmt.Println("相机停止工作")
}
type Computer struct{
}
func (c Computer) Working(usb Usb) {
usb.Start()
if ph,ok :=usb.(phone); ok{
ph.Call()
}
usb.Stop()
}
func main() {
var usb [3]Usb
fmt.Println(usb)
usb[0] = phone{"小米"}
usb[1] = Cammer{"ccd"}
usb[2] = phone{"华为"}
var com Computer
for _, v := range usb {
com.Working(v)
}
/* 手机开始工作
正在打电话
手机停止工作
相机开始工作
相机停止工作
手机开始工作
正在打电话
手机停止工作 */
}
- 用断言判断变量类型
func TypeJudge(items... interface{}) {
for _, v := range items {
switch v.(type) {
case bool :
fmt.Println("bool")
case float32 :
fmt.Println("float32")
case float64 :
fmt.Println("float64")
case int, int32, int64 :
fmt.Println("int系列")
case string :
fmt.Println("string")
case Student :
fmt.Println("Student")
case *Student :
fmt.Println("*Student")
default :
fmt.Println("不确定")
}
}
}
面向对象三大特性
封装
go并没有特别强调封装,本身是对面向对象特性做简化
把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序其他包只能通过被授权的方法,才能对字段进行操作
- 隐藏实现细节(使用时直接调用即可)
- 可以对数据进行验证,保证安全合理
- 对结构体中属性进行封装
- 通过方法,包实现封装
package mod
import "fmt"
//将属性首字母小写
type person struct{
Name string
age int
sal float64
}
//提供工程模式的函数
func NewPerson(name string) *person {
return &person{
Name : name,
//因为age和sal不是对外公开的所以不能直接访问
//如果在此处赋值可以通过指针直接查看到对应的值
}
}
//首字母大写的Set和Get方法,可以加入数据验证的业务逻辑
func (p *person) SetAge(age int) {
if age >18 && age <150{
p.age = age
return
}
fmt.Println("年龄不正确")
}
func (p *person) GetAge() int {
return p.age
}
func (p *person) SetSal(sal float64) {
if sal >=3000 && sal <= 30000{
p.sal = sal
return
}
fmt.Println("薪资不正确")
}
func (p *person) GetSal() float64 {
return p.sal
}
package main
import (
"fmt"
"Ajinjie/day3/mod"
)
func main() {
p := mod.NewPerson("张三")
p.SetAge(20)
p.SetSal(6666)
fmt.Println(p)
// &{张三 20 6666}
}
继承
解决代码复用,通过嵌套匿名结构体的方式,直接访问匿名结构体的属性和方法
- 当多个结构体有相同的属性和方法时,抽象出一个结构体,在该结构体中定义相同的属性和方法
- 其他结构体不需重新定义这些属性和方法,只需struct嵌套另一个匿名结构体
- 结构体可以使用嵌套匿名结构体所有的字段和方法,无论首字母大写或者小写
//利用继承解决代码冗余
type Student struct{
Name string
age int
Score int
}
func (s *Student) showInfo() {
fmt.Println("姓名:",s.Name,"年龄:",s.age,"成绩:",s.Score)
}
func (s *Student) SetScore(score int){
s.Score = score
}
type Pupil struct {
Student
}
func (p *Pupil) testing1() {
fmt.Println("小学生正在考试")
}
type Grudate struct {
//没有变量名
Student
}
func (g *Grudate) testing2() {
fmt.Println("大学生正在考试")
}
func main() {
pupil := &Pupil{}
pupil.Student.Name = "周六"
pupil.Student.age = 6
pupil.testing1()
pupil.Student.SetScore(66)
pupil.Student.showInfo()
/* 小学生正在考试
姓名: 周六 年龄: 6 成绩: 66 */
grudate := &Grudate{}
//编译器在当前结构体找不到对应的属性和方法时
//会自动到嵌入的匿名结构体中找
grudate.Name = "王五"
grudate.age = 22
grudate.testing2()
grudate.SetScore(222)
grudate.showInfo()
/* 大学生正在考试
姓名: 王五 年龄: 22 成绩: 222 */
}
- 当结构体和匿名结构体有相同的属性或者方法时,编译器采用就近访问原则,若要访问匿名结构体的属性或者方法,可以通过匿名结构体名来区分
type A struct {
Name string
age int
}
func (a *A) SayOk () {
fmt.Println("A SayOk",a.Name)
}
func (a *A) hello () {
fmt.Println("A hello",a.Name)
}
type B struct{
A
Name string
}
func (b *B) SayOk () {
fmt.Println("A SayOk",b.Name)
}
func main() {
var b B
//会直接赋值到B结构体的Name变量
//A中Name不会被赋值
b.Name = "麻子"
b.A.Name = "大娃"
b.age = 18
b.SayOk()
b.hello()
/* A SayOk 麻子
A hello */
/* A SayOk 麻子
A hello 大娃 */
}
- 结构体嵌入多个匿名结构体,匿名结构体中有相同的字段和方法(结构体本身没有同名字段和方法),在访问时,必须明确匿名结构体名字,否则报错
type A struct {
Name string
age int
}
type B struct {
Name string
score float64
}
type C struct {
A
B
}
func main() {
var c C
// ambiguous selector c.Name
//当C结构体中也有Name则不会报错
//因为编译器根据就近原则直接找到要赋值的变量
c.A.Name ="张三"
fmt.Println(c)
// {{张三 0} { 0}}
}
- struct嵌套有名结构体,这种模式为组合,在访问组合的结构体字段或者方法时,必须带上结构体的名字
type A struct{
Name string
age int
}
type B struct {
//不是匿名结构体
//当要找当前结构体中没有的属性或方法时,会直接报错
//因为编译器不会向上找属性或方法
a A
}
func main() {
var b B
/* b.Name undefined (type B has no field
or method Name) */
// b.Name = "张三"
//要访问有名结构体字段或方法,必须带上而结构体名
b.a.Name = "张三"
fmt.Println(b)
// {{张三 0}}
}
- 嵌套匿名结构体后,可以在创建结构体实例时,直接指定各个匿名结构体字段的值
- struct嵌套多个匿名结构体,该结构体可以直接访问嵌套匿名结构体的字段和方法,实现多重继承
type Goods struct {
Name string
price float64
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}
type Latiao struct {
*Goods
*Brand
}
func main() {
tv :=TV{
Goods{
Name : "电视机",
price : 9999.9,
},
Brand{"美的","大北极"},
}
fmt.Println(tv)
// {{电视机 9999.9} {美的 大北极}}
latiao := Latiao{
&Goods{
Name : "卫龙超级大辣条特惠仅需",
price : 6.66,
},
&Brand{"卫龙","卫龙总部"},
}
fmt.Println(latiao)
// {0xc000008078 0xc000026400}
fmt.Println(*latiao.Goods," ",*latiao.Brand)
// {卫龙超级大辣条特惠仅需 6.66} {卫龙 卫龙总部}
}
- 结构体的匿名字段是基本数据类型,该类型匿名字段只能有一个
type Test struct{
int
n int
}
func main() {
var t Test
t.int = 66
t.n = 99
fmt.Println(t)
// {66 99}
}
多态
变量(实例)具有多种形态,是通过接口实现,按照统一的接口调用不同的实现,这是接口变量就呈现不同的形态
- 多态参数
Usb接口案例 - 多态数组
type Usb interface{
Start()
Stop()
}
type phone struct{
Name string
}
func (p phone) Start() {
fmt.Println("手机开始工作")
}
func (p phone) Stop() {
fmt.Println("手机停止工作")
}
type Cammer struct{
Name string
}
func (c Cammer) Start() {
fmt.Println("相机开始工作")
}
func (c Cammer) Stop() {
fmt.Println("相机停止工作")
}
func main() {
//普通数组只能存放一种指定类型的数据
//可以通过接口,实现存放多种类型的数据
var usb [3]Usb
fmt.Println(usb)
usb[0] = phone{"小米"}
usb[1] = Cammer{"ccd"}
usb[2] = phone{"华为"}
fmt.Println(usb)
/* [<nil> <nil> <nil>]
[{小米} {ccd} {华为}] */
}
文件
文件在程序中是以流的形式操作的
流:数据在数据源(文件)和程序(内存)之间经历的路径
- 输入流:数据从数据源到程序的路径(读文件)
- 输出流:数据从程序到数据源的路径(写文件)
import (
"fmt"
"os"
)
func main() {
//打开文件
file , err :=os.Open("G:/goStudy/src/Ajinjie/document/test.txt")
if err != nil{
fmt.Println("open file err",err)
}
//输出文件
fmt.Println(file)
// &{0xc000100780}
/* open file err open document/test.txt:
The system cannot find the path specified.
<nil> */
//关闭文件
err = file.Close()
if err != nil{
fmt.Println("close file err",err)
}
}
读文件
- 带缓冲区的Reader读文件
import (
"fmt"
"os"
"bufio"
"io"
)
func main() {
file , err :=os.Open("G:/goStudy/src/Ajinjie/document/test.txt")
if err != nil{
fmt.Println("open file err",err)
}
//当函数退出时,及时关闭file,否则会有内存泄露
defer file.Close()
//创建Reader,带缓冲
const (
defaultBufsize = 4096 //默认的缓冲区为4096
//缓冲区可以使读取文件时读一部分处理一部分
//而不直接将文件读到内存中,使得可以处理比较大的文件
)
reader :=bufio.NewReader(file)
for {
//循环读取文件内容,读到换行结束一次
str ,err :=reader.ReadString('\n')
//io.EOF表示读取到文件末尾
if err == io.EOF {
break
}
//在读取时会将换行符也读取到
fmt.Print(str)
//最后一行的末尾不是\n,是EOF
/* hello world
我爱北京
go,hello bad! */
}
fmt.Println("文件读取结束")
// 文件读取结束
}
- 读取文件内容并显示在终端使用ioutil包,一次将整个文件读入到内存,适用于文件不大的情况,同时不需自己打开和关闭文件
import (
"fmt"
"io/ioutil"
)
func main() {
//一次性读取
file := "G:/goStudy/src/Ajinjie/document/test.txt"
content, err :=ioutil.ReadFile(file)
// ReadFile内有封装的文件打开和关闭
if err != nil{
fmt.Println("read err",err)
}
//输出的是[]byte切片
fmt.Println(content)
/* [104 101 108 108 111 32 119 111 114 108 100 13 10 230
136 145 231 136 177 229 140 151 228 186 172 13 10 103
111 44 104 101 108 108 111 32 98 97 100 33 13 10] */
fmt.Println(string(content))
//在读取的时候EOF行也被读取到
/* hello world
我爱北京
go,hello bad!
*/
}
写文件
OpenFile更一般性的文件打开函数,使用指定选项,指定模式打开指定名称的文件,操作成功返回文件对象,失败,错误底层类型是*PathError
- 创建新文件写入
import (
"fmt"
"bufio"
"os"
)
func main() {
//文件名,打开方式(可以组合使用),权限控制(windows下没用)
//创建新文件
filePath :="G:/goStudy/src/Ajinjie/document/wr.txt"
//打开文件
//写文件模式,该文件不存在就创建该文件
file, err :=os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件打开失败",err)
return
}
//关闭文件句柄
defer file.Close()
//写入
//写入,因为编辑器对换行的识别不同,例如只写\n记事本中无法换行
str :="hello,go\r\n"
//使用带缓存的方式写文件
writer :=bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
//因为是带有缓存的写入
//内容一开始在缓存区,所以需要Flush方法写入磁盘
//否则文件中不存在数据
writer.Flush()
}
- 覆盖原文件写入
import (
"fmt"
"bufio"
"os"
)
func main() {
filePath :="G:/goStudy/src/Ajinjie/document/wr.txt"
//写文件模式,并且清空原文件
file, err :=os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件打开失败",err)
return
}
defer file.Close()
str :="你好!chain\r\n"
writer :=bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
writer.Flush()
}
- 在原内容上追加
import (
"fmt"
"bufio"
"os"
)
func main() {
filePath :="G:/goStudy/src/Ajinjie/document/wr.txt"
//写文件模式,并且在原文件后追加
file, err :=os.OpenFile(filePath, os.O_WRONLY | os.O_APPEND, 0666)
if err != nil {
fmt.Println("文件打开失败",err)
return
}
defer file.Close()
str :="ABCDEFG\r\n"
writer :=bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
writer.Flush()
}
- 将原文内容显示在终端,并且追加文本
import (
"fmt"
"bufio"
"os"
"io"
)
func main() {
filePath :="G:/goStudy/src/Ajinjie/document/wr.txt"
//读写文件模式,并且在原文件后追加
file, err :=os.OpenFile(filePath, os.O_RDWR | os.O_APPEND, 0666)
if err != nil {
fmt.Println("文件打开失败",err)
return
}
defer file.Close()
//读取原文件内容,并显示
reader := bufio.NewReader(file)
for {
str, err :=reader.ReadString('\n')
if err == io.EOF{
break
}
fmt.Print(str)
/* 你好!chain
你好!chain
你好!chain
你好!chain
你好!chain
ABCDEFG
ABCDEFG
ABCDEFG
ABCDEFG
ABCDEFG */
}
str :="上学ing~~~\r\n"
writer :=bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
writer.Flush()
}
- 将一个文件内容,写入另一个文件(两文件已存在)
import (
"fmt"
"io/ioutil"
)
func main() {
//将文件内容读取到内存
//将读取到的内容写入
filePath :="G:/goStudy/src/Ajinjie/document/wr.txt"
filePath1 :="G:/goStudy/src/Ajinjie/document/test.txt"
content,err :=ioutil.ReadFile(filePath)
if err != nil{
fmt.Println("读取错误",err)
return
}
//直接传入切片
//会清空原文件写入
err = ioutil.WriteFile(filePath1,content,0666)
if err != nil{
fmt.Println("写入错误",err)
}
}
判断文件是否存在
import (
"fmt"
"os"
)
func PathExists (path string) (bool,error) {
_,err := os.Stat(path)
//返回的错误为nil,说明文件或文件夹存在
if err == nil {
return true,nil
}
//返回的错误类型使用IsNotExist判断为true,说明文件或文件夹不存在
if os.IsNotExist(err) {
return false,nil
}
//返回的错误为其他类型,不确定是否存在
return false,err
}
拷贝文件
import (
"fmt"
"io"
"os"
"bufio"
)
func CopyFile(dstFileName string,srcFileName string) (written int64,err error) {
srcfile, err :=os.Open(srcFileName)
if err !=nil {
fmt.Println("文件打开错误",err)
}
defer srcfile.Close()
reader :=bufio.NewReader(srcfile)
//因为文件可能不存在
dstfile, err1 :=os.OpenFile(dstFileName, os.O_WRONLY | os.O_CREATE, 0666)
if err1 !=nil {
fmt.Println("文件打开错误",err1)
}
defer dstfile.Close()
writer :=bufio.NewWriter(dstfile)
//里面是有缓存的所以可以操作大文件
return io.Copy(writer,reader)
}
func main() {
srcfile :="F:/downloads/未命名文件.png"
/* open G:/goStudy/src/Ajinjie/document: is
a directory */
// dstfile :="G:/goStudy/src/Ajinjie/document"
dstfile :="G:/goStudy/src/Ajinjie/document/image.png"
_,err :=CopyFile(dstfile,srcfile)
if err == nil {
fmt.Println("拷贝完成")
}else {
fmt.Println(err)
}
// 拷贝完成
}
统计字符
import (
"fmt"
"io"
"os"
"bufio"
)
type charCount struct{
chCount int64 //英文个数
numCount int64 //数字个数
spaceCount int64 //空格个数
otherCount int64 //其他个数
}
func main() {
filePath := "G:/goStudy/src/Ajinjie/document/test.txt"
file, err :=os.Open(filePath)
if err != nil {
fmt.Println("打开失败",err)
return
}
defer file.Close()
var count charCount
reader :=bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF{
break
}
// 默认遍历str字符串时按一个字符遍历
for _, v := range str {
// v的类型:int32,rune切片
//byte切片是uint8
//做if分支函数使用
switch {
case v >= 'a' && v<='z':
fallthrough
case v >= 'A' && v<='Z':
count.chCount++
case v == ' ' && v == '\t':
count.spaceCount++
case v >= '0' && v<='9':
count.numCount++
default :
count.otherCount++
}
}
}
//换行末尾有\r\n
fmt.Println(count)
// {9 7 0 25}
//[]rune是基于字符串创建的,所以需要先转为字符串,int32位切片
// strRune := []rune(str)
}
命令行参数
获取到命令行输入的各种参数
- 利用切片获取参数
import (
"fmt"
"os"
)
func main() {
//获取到命令行里的动态参数
// .\test.exe com G:\goStudy\src\Ajinjie\document\test.txt 999
//获取到的顺序会严格按照参数顺序
fmt.Println("命令行参数:",len(os.Args))
for _, v := range os.Args {
fmt.Println(v)
}
/* 命令行参数: 4
G:\goStudy\src\Ajinjie\day7\test.exe
com
G:\goStudy\src\Ajinjie\document\test.txt
999 */
}
- flag包解析命令行,参数顺序可以随意
import (
"fmt"
"flag"
)
func main() {
//定义变量接收命令行参数值
var user string
var pwd string
var host string
var port int
flag.StringVar(&user,"u","","获取用户名,默认为空")
flag.StringVar(&pwd,"pwd","","获取密码,默认为空")
flag.StringVar(&host,"h","localhost","获取主机名,默认为localhost")
flag.IntVar(&port,"port",3306,"获取端口号,默认为3306")
//从切片中解析注册的flag,在所有flag注册好后,并没有访问前执行
flag.Parse()
fmt.Println("user=",user," ","pwd=",pwd," ","host=",host," ","port=",port)
/* .\flag.exe -u root -pwd root -h 127.0.0.5 -port 8080
user= root pwd= root host= 127.0.0.5 port= 8080 */
}
json
轻量级的数据交换格式,易于阅读和编写,易于机器解析和生成,使用键值对保存数据(任何数据类型)
- 在网络传输时将数据序列化成json字符串,到接收方得到json字符串时,反序列化恢复为原始数据类型
序列化
import (
"fmt"
"encoding/json"
)
type Monster struct{
Name string
Age int
//指定json字符串的key属性名称,利用反射机制
/* Name string `json:"monster_name"`
Age int `json:"monster_age"` */
Birthday string
Sal float64
Skill string
}
func testStruct() {
monster := Monster{
Name : "张三",
Age : 18,
Birthday : "2006-06-06",
Sal : 6666.66,
Skill : "挣钱",
}
//序列化
// Marshal的参数是空接口,所以任何类型都可以
//[...]int是数组类型,...是一个语法糖,让编译器根据初始化时提供的元素个数来确定数组长度
// 返回byte类型切片
//json.Marshal跨包使用结构体,如果属性名首字母小写不会报错,但无法序列化处理,该属性会被丢弃
data,err :=json.Marshal(&monster)
if err != nil {
fmt.Println("序列化错误",err)
}
fmt.Println("struct序列化:",string(data))
}
func testMap() {
var a map[string]interface{}
a = make(map[string]interface{})
a["name"] = "周六"
a["age"] = 20
a["address"] = [2]string{"猫","极道"}
//make是指针类型
data,err :=json.Marshal(a)
if err != nil {
fmt.Println("序列化错误",err)
}
fmt.Println("map序列化:",string(data))
}
func testSlice() {
var slice []map[string]interface{}
var m1 map[string]interface{}
m1 = make(map[string]interface{})
m1["name"] = "李四"
m1["age"] = 30
m1["address"] = "猫"
slice = append(slice,m1)
var m2 map[string]interface{}
m2 = make(map[string]interface{})
m2["name"] = "钱五"
m2["age"] = 35
m2["address"] = "猫"
slice = append(slice,m2)
data,err :=json.Marshal(slice)
if err != nil {
fmt.Println("序列化错误",err)
}
fmt.Println("slice序列化:",string(data))
}
func testFloat64() {
var num float64 = 234.56
//也可以传入非指针类型
data,err :=json.Marshal(num)
if err != nil {
fmt.Println("序列化错误",err)
}
fmt.Println("float64序列化:",string(data))
}
func main() {
//序列化后的顺序可能是不一样的
//map 结构体和切片序列化
testStruct()
/* {"Name":"张三","Age":18,"Birthday":"2006-06-06"
,"Sal":6666.66,"Skill":"挣钱"} */
/* struct序列化: {"monster_name":"张三","monster_age":18
,"Birthday":"2006-06-06","Sal":6666.66,"Skill":"挣钱"} */
testMap()
// map序列化: {"address":["猫","极道"],"age":20,"name":"周六"}
testSlice()
/* slice序列化: [{"address":"猫","age":30,"name":"李四"}
,{"address":"猫","age":35,"name":"钱五"}] */
testFloat64()
//相当于转为了字符串
// float64序列化: 234.56
}
反序列化
- 反序列json字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致
- json字符串通过程序获取是不需要转义双引号的,因为程序内部会自动处理
import (
"fmt"
"encoding/json"
)
type Monster struct{
Name string
Age int
Birthday string
Sal float64
Skill string
}
func unmarshalStruct() {
str :="{\"Name\":\"张三\",\"Age\":18,\"Birthday\":\"2006-06-06\",\"Sal\":6666.66,\"Skill\":\"挣钱\"}"
var monster Monster
err :=json.Unmarshal([]byte(str),&monster)
if err != nil {
fmt.Println("反序列化错误:",err)
}
fmt.Println("struct反序列化:",monster)
}
func unmarshalMap() {
str :="{\"address\":[\"猫\",\"极道\"],\"age\":20,\"name\":\"周六\"}"
var a map[string]interface{}
//在反序列化map时,底层自动make,封装到Unmarshal函数
err :=json.Unmarshal([]byte(str),&a)
if err != nil {
fmt.Println("反序列化错误:",err)
}
fmt.Println("map反序列化:",a)
}
func unmarshalSlice() {
str :="[{\"address\":\"猫\",\"age\":30,\"name\":\"李四\"},"+
"{\"address\":\"猫\",\"age\":35,\"name\":\"钱五\"}]"
var slice []map[string]interface{}
err :=json.Unmarshal([]byte(str),&slice)
if err != nil {
fmt.Println("反序列化错误:",err)
}
fmt.Println("slice反序列化:",slice)
}
func main() {
unmarshalStruct()
// struct反序列化: {张三 18 2006-06-06 6666.66 挣钱}
unmarshalMap()
// map反序列化: map[address:[猫 极道] age:20 name:周六]
unmarshalSlice()
/* slice反序列化: [map[address:猫 age:30 name:李四]
map[address:猫 age:35 name:钱五]] */
}
单元测试
go中自带有轻量级的测试框架testing和自带的go test命令实现单元测试和性能测试,写相对应的函数测试用例和压力测试用例
- 确保函数是可运行且运行结果是正确的
- 确保代码性能好的
- 及时发现逻辑错误
import _"fmt"
func addUpper(n int) int {
res := 0
for i := 1; i <= n; i++ {
res += i
}
return res
}
import (
_"fmt"
//引入go的testing框架包
"testing"
)
//编写测试用例
//Test后的第一个首字母必须大写
func TestAddUpper(t *testing.T) {
res := addUpper(10)
if res != 55{
// fmt.Println("期望值:",55,"实值:",res)
//输出日志的同时停止程序
/* (*testing.common).Fatalf call has
arguments but no formatting directives */
t.Fatalf("期望值:%v,实值:%v",55,res)
}
//正确输出日志
t.Logf("执行正确")
// 执行正确
}
- 一个测试用例文件中,可以有多个测试用例函数
- go test(运行正确,无日志,错误时,输出日志)
- go test -v(都输出日志)
- 测试单个文件时,需要写入测试文件和被测试的原文件
串行一致
指在多线程或并发环境中,程序的执行结果与按照某种顺序串行执行的结果相同。尽管多个协程可能同时运行,但从外部观察,它们的执行顺序看起来是有序的,就像它们是依次执行的一样,程序执行结果和预期一致
- 串行:任务或操作按照预定的顺序依次执行,每个任务在开始之前必须等待前一个任务完成
- 互斥锁(sync.Mutex)
- 读写锁(sync.RWMutex)
- 条件变量(sync.Cond)等同步原语
- 通道(channel)
gorutine
- 并发:多线程程序在单核上运行
- 并行:多线程程序在多核上运行
- go的协程机制可以轻松开启上万个协程,其他语言的并发机制一般是基于线程的,属于内核态,资源耗费较大
go主线程
- 可理解为进程,一个go线程上可起有多个协程
- 主线程是一个物理线程,直接作用在CPU上,是重量级的
go协程
- 协程从主线程开启,轻量级线程,是逻辑态
- 独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 轻量级线程,是编译器的底层优化
- 使用
time.Sleep()
import (
"fmt"
"time"
)
//每个1秒输出helloworld
func test() {
for i := 1; i <=10; i++ {
fmt.Println("test hello,world",i)
//休眠
time.Sleep(time.Second)
}
}
func main() {
//test执行完后再执行main
// test()
//test和main穿插执行
//开启一个协程
go test()
for i := 1; i <=10; i++ {
fmt.Println("main hello,world",i)
time.Sleep(time.Second)
}
/* main hello,world 1
test hello,world 1
test hello,world 2
main hello,world 2
main hello,world 3
test hello,world 3
test hello,world 4
main hello,world 4 */
}
sync.WaitGroup
一种同步机制
sync包中提供常用的并发原语
WaitGroup 忽略并发操作的结果或者有其他方式收集并发操作的结果
import (
"fmt"
"sync"
)
//声明全局等待组变量
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("hello", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 阻塞,等待所有登记的goroutine都结束
}
sync.Once
确保某些操作即使在高并发的场景下也只会执行一次
内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成
//要执行的函数f需要传递参数就需要搭配闭包使用
func (o *Once) Do(f func())
//实现并发安全的单例模式
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
sync.Map
开箱即用的并发安全版 map,表示不需要make函数初始化就能够直接使用
当多个 goroutine 中并发对内置的 map 进行读写操作,会存在数据竞争问题
- Store(key, value interface{}) 存储key-value数据
- Load(key interface{}) (value interface{}, ok bool) 查询key对应的value
- LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 查询或存储key对应的value
- LoadAndDelete(key interface{}) (value interface{}, loaded bool) 查询并删除key
- Delete(key interface{}) 删除key
- Range(f func(key, value interface{}) bool) 对map中的每个key-value依次调用f
import (
"fmt"
"strconv"
"sync"
)
// 并发安全的map
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
// 对m执行20个并发的读写操作
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) // 存储key-value
value, _ := m.Load(key) // 根据key取值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
调度模型(MPG模式)
- M:操作系统的主线程(物理线程)
- 当多个运行在一个CPU并发,多个并行
- P:协程执行需要的上下文
- 所需资源和当前系统运行状态
- 根据当前情况开启协程工作
- G:协程
- 可能有协程队列等待执行
- 当协程G0阻塞时,可能会创建主线程M1(也可能从已有线程池取出),将等待的协程挂到该主线程M1下开始执行,主线程M0继续执行协程G0
- 这样的调度模式,既可以让协程G0继续执行,同时不会让队列的其他协程一直阻塞
- 到协程G0不阻塞后,主线程M0被放到空闲的主线程继续执行,同时协程G0被唤醒
协程泄露
指的是协程在执行过程中未能正常退出,导致系统协程数量不断上升,最终可能导致程序崩溃或系统资源耗尽,内存和CPU的浪费
并发安全和锁
多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)
资源竞争报错
import (
"fmt"
"time"
)
//计算阶乘并放入map
var (
myMap = make(map[int]int,10)
)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= 1
}
//多个协程同时写入
// error: concurrent map writes
myMap[n] = res
}
func main() {
//开启多个协程,将统计结果放入map
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(time.Second*10)
//马上遍历可能会没有数据,因为主线程先执行完毕
fmt.Println(myMap)
// go build -race 当有资源竞争时输出信息
// 要在环境变量配置CGO_ENABLED=1
}
全局变量加互斥锁同步
- 等待的时间不确定
- 主线程休眠时间长,会加长等待时间,等待时间短,可能还有协程处于工作状态,被迫随主线程的退出而销毁
- 并不利于多个协程对全局变量的读写操作
import (
"fmt"
"time"
"sync"
)
//计算阶乘并放入map
//限制每次只有一个协程可以修改这个全局变量
var (
myMap = make(map[int]int,10)
//声明一个全局互斥锁
//sync 是包:synchornized 同步
//Mutex 互斥
lock sync.Mutex
)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//多个协程同时写入
// error: concurrent map writes
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
func main() {
//开启多个协程,将统计结果放入map
//200数据太大无法存储
for i := 1; i <= 20; i++ {
go test(i)
}
time.Sleep(time.Second*10)
//马上遍历可能会没有数据,因为主线程先执行完毕
fmt.Println(myMap)
//map[1:1 2:2 3:6 4:24 5:120 6:720 7:5040 8:40320 9:362880 10:3628800 11:39916800 12:479001600 13:6227020800 14:87178291200 15:1307674368000 16:20922789888000 17:355687428096000 18:6402373705728000 19:121645100408832000 20:2432902008176640000]
// go build -race 当有资源竞争时输出信息
// 要在环境变量配置CGO_ENABLED=1
//因为在实际运行中,主线程可能不知道10秒执行完所有协程
//因此底层可能仍然出现资源竞争
lock.Lock()
for k, v := range myMap {
fmt.Println(k,":",v)
}
lock.Unlock()
/* 3 : 6
2 : 2
14 : 87178291200
18 : 6402373705728000
4 : 24
6 : 720
7 : 5040
12 : 479001600
*/
}
读写互斥锁
很多场景是读多写少的,当并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的
- 一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待
- 一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待
Lock() 获取写锁
Unlock() 释放写锁
RLock() 获取读锁
RULock() 释放读锁
RLocker() Locker 返回一个实现Locker接口的读写锁
var (
x int64
wg sync.WaitGroup
rwMutex sync.RWMutex
)
// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
rwMutex.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwMutex.Unlock() // 释放写锁
wg.Done()
}
// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
rwMutex.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwMutex.RUnlock() // 释放读锁
wg.Done()
}
func do(wf, rf func(), wc, rc int) {
start := time.Now()
// wc个并发写操作
for i := 0; i < wc; i++ {
wg.Add(1)
go wf()
}
// rc个并发读操作
for i := 0; i < rc; i++ {
wg.Add(1)
go rf()
}
wg.Wait()
cost := time.Since(start)
fmt.Printf("x:%v cost:%v\n", x, cost)
}
channel
goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制
- 引用类型
- 一种数据结构——队列(FIFD)
- 本身是线程安全的,不需要加锁
- 有类型的,只能存放一种类型的数据,要实现多种类型存放需要利用接口实现
初始化,写入和读取
//定义
var intChan chan int
intChan = make(chan int,3)
fmt.Printf("chan本身的地址:%p,chan的值(指向的地址):%v \n",&intChan,intChan)
/* chan本身的地址:0xc00000a028,chan的值(指向的地址):0xc000076100 */
//写入数据
intChan<- 10
num := 211
intChan<- num
//长度和容量,管道不能自增长
fmt.Printf("chan的长度:%v,chan的容量:%v \n",len(intChan),cap(intChan))
// chan的长度:2,chan的容量:3
intChan<- 66
//超出容量报错
// intChan<- 99
// fatal error: all goroutines are asleep - deadlock!
//管道可以边放边取
var num2 int
num2 = <-intChan
fmt.Println(num2)
fmt.Printf("chan的长度:%v,chan的容量:%v \n",len(intChan),cap(intChan))
/* 10
chan的长度:2,chan的容量:3 */
//在没有协程的情况下,管道数据全部取出,再次取出报错
num3 := <-intChan
num4 := <-intChan
fmt.Println(num3," ",num4)
// 211 66
num5 := <-intChan
// fatal error: all goroutines are asleep - deadlock!
fmt.Println(num5)
各类型管道案例
type Cat struct {
Name string
Age int
}
func main() {
var mapChan chan map[string]string
mapChan = make(chan map[string]string,10)
m1 := make(map[string]string,20)
m1["city1"] = "北京"
m1["city2"] = "上海"
m2 := make(map[string]string,20)
m2["hero1"] = "蜘蛛侠"
m2["hero2"] = "钢铁侠"
mapChan <- m1
mapChan <- m2
/* invalid operation: cannot indirect mapChan
(variable of type chan map[string]string) */
// fmt.Println("mapChan:",*mapChan)
var catChan chan Cat
catChan = make(chan Cat,10)
cat1 := Cat{"tom",18}
cat2 := Cat{"jery",20}
catChan <- cat1
catChan <- cat2
var catChan1 chan *Cat
catChan1 = make(chan *Cat,10)
catChan1 <- &cat1
catChan1 <- &cat2
var allChan chan interface{}
allChan = make(chan interface{},10)
allChan <- cat1
allChan <- 10
allChan <- "张三"
allChan <- m1
newCat := <-allChan
//在运行的时候可以确定是Cat类型不会报错
fmt.Printf("newCat的类型:%T,newCat的值:%v \n",newCat,newCat)
//在编译时此处仍认为是空接口类型,所以会报错
/* newCat.Name undefined (type interface{}
has no field or method Name) */
a := newCat.(Cat)
fmt.Println("名字:",a.Name)
/* newCat的类型:main.Cat,newCat的值:{tom 18}
名字: tom */
}
关闭
使用内置close函数关闭channel,当关闭后,就不能再向channel写数据,但仍然可以读取数据
- 对一个关闭的通道再发送值就会导致 panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致 panic
intChan :=make(chan int,3)
intChan <- 99
intChan <- 66
close(intChan)
// panic: send on closed channel
// intChan <- 88
n1 := <-intChan
fmt.Println("ok",n1)
// ok 99
遍历
- 在遍历时,channel没有关闭,则会出现死锁deadlock错误
- 已经关闭,则会正常遍历数据,遍历完后退出遍历
//普通for不要使用,因为管道长度会变化
intChan :=make(chan int,50)
for i := 0; i <50; i++ {
intChan <- i*2
}
//因为管道里不会返回下标,因为它只能顺序遍历
//如果没有关闭,则会一直遍历,因为没有结束的标志
//一直等待就会报错
// fatal error: all goroutines are asleep - deadlock!
close(intChan)
for v := range intChan {
fmt.Println("v=",v)
}
无缓冲通道
无缓冲通道ch上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 创建一个 goroutine 从通道接收值
ch <- 10
fmt.Println("发送成功")
}
有缓冲通道
通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
多返回值模式
在接收操作时判断一个通道是否被关闭
- value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
- ok:通道ch关闭时返回 false,否则返回 true
value, ok := <- ch
for range 接收值
当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环
func f3(ch chan int) {
for v := range ch {
fmt.Println(v)
}
}
goroutine+channel
func writeData (intChan chan int){
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("write=",i)
}
//用close做结束标记
close(intChan)
}
func readData(intChan chan int,exitChan chan bool ) {
for{
//ok取值是否成功,当读到关闭时读取失败
v,ok := <- intChan
if !ok {
break
}
fmt.Println("v=",v)
}
//读取完成后
exitChan <- true
close(exitChan)
}
func main() {
intChan := make(chan int)
exitChan := make(chan bool)
//读的快,写的慢不会报错,在阻塞时编译器发现有在读取,不会发生死锁
go writeData(intChan)
go readData(intChan,exitChan)
//没有做任何工作主线程会很快停止工作
for{
_,ok := <- exitChan
if !ok{
break
}
}
//读和写协程是交替无顺序执行
/* write= 1
v= 1
v= 2
write= 2
write= 3
v= 3
v= 4
write= 4
write= 5 */
}
阻塞
不停往里写而不读取
func writeData (intChan chan int){
for i := 1; i <= 50; i++ {
//在这里阻塞
intChan <- i
fmt.Println("write=",i)
}
//用close做结束标记
close(intChan)
}
func main() {
intChan := make(chan int)
exitChan := make(chan bool)
go writeData(intChan)
// fatal error: all goroutines are asleep - deadlock!
// go readData(intChan,exitChan)
}
单向通道
可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,保证数据安全
- channel可以声明为只读,或者只写性质,属于变量的属性,而不是类型的一部分
//只写
var intChan chan<- int
//无缓冲管道,在通信时要求读取和写入同时准备好
// intChan = make(chan int)
//有缓冲管道,可以在没有读取操作的情况下缓存一定数量的数据
intChan = make(chan int,2)
intChan <- 66
/* invalid operation: cannot receive from
send-only channel intChan (variable of type chan<- int) */
// num := <- intChan
// fmt.Println(num)
//只读
var intChan1 <-chan int
// fatal error: all goroutines are asleep - deadlock!
// goroutine 1 [chan send]:
// intChan1 = make(<-chan int,2)
intChan1 = make(chan int,2)
/* invalid operation: cannot send to
receive-only channel intChan1 (variable of type <-chan int) */
// intChan1 <- 66
num := <- intChan1
fmt.Println(num)
//传入管道后,使该管道只能写入
func writeData (intChan chan<- int){
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("write=",i)
}
//用close做结束标记
close(intChan)
}
func main() {
intChan := make(chan int,2)
go writeData(intChan)
}
select 多路复用
可能需要同时从多个通道接收数据,没有数据接收时当前写成会发生阻塞
使用select可以解决从管道读取数据的阻塞问题,响应多个通道操作(当不使用close关闭管道,一直读取后造成)
- 可处理一个或多个 channel 的发送/接收操作。
- 如果多个 case 同时满足,select 会随机选择一个执行。
- 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
intChan :=make(chan int,10)
for i := 0; i <10; i++ {
intChan <- i
}
stringChan :=make(chan string,5)
for i := 0; i <5; i++ {
stringChan <- "hello"+fmt.Sprintf("%d",i)
}
//传统遍历需要关闭管道,但关闭时机不好确定
label:
for{
select{
//管道一直没有关闭,不会一直阻塞
//会自动到下一个case匹配
case v := <- intChan :
fmt.Println("intChan读取到数据:",v)
case v := <- stringChan :
fmt.Println("stringChan读取到数据:",v)
default :
fmt.Println("都取不到")
//退出函数
// return
break label
}
}
/* intChan读取到数据: 0
stringChan读取到数据: hello0
stringChan读取到数据: hello1
intChan读取到数据: 1
intChan读取到数据: 2
stringChan读取到数据: hello2
intChan读取到数据: 3
intChan读取到数据: 4 */
//输出10以内的奇数
ch := make(chan int, 1)
for i := 1; i <= 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
- 使用recover,解决协程中出现panic,导致程序崩溃问题
import (
"fmt"
"time"
)
func sayHello(){
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
func test() {
// 使用defer+recover
// 在协程出现恐慌时不影响其他
defer func () {
if err := recover();err !=nil {
fmt.Println("test painc:",err)
}
}()
var myMap map[int]string
// panic: assignment to entry in nil map
myMap[0]="golang"
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("hello,main")
time.Sleep(time.Second)
}
/* hello,main
test painc: assignment to entry in nil map
hello,main
hello,world */
}
原子操作
在多线程或并发环境中,一个操作在执行过程中不会被其他操作中断,确保操作的完整性和一致性,由内置的标准库sync/atomic提供
反射
在程序运行期间对程序本身进行访问和修改的能力。在编译时,变量名不会被写入到可执行部分,所以运行时,程序无法获取自身信息
- 可以在运行时动态获取变量的各种信息,比如变量的类型,类别
- 结构体变量,可以获取到结构体本身的信息(字段和方法)
- 可以修改变量的值,调用关联的方法
- reflect.Type(变量名),获取变量的类型,返回reflect.Type类型
- reflect.Value(变量名),获取变量的值,返回reflect.Value类型,是一个结构体类型,通过reflect.Value,可以获取到关于该变量的信息
- 变量,interface{}和reflect.Value是可以相互转换的
TypeOf
主要用于获取变量的类型信息。例如,可以用来判断变量的类型、获取类型的名称、字段信息等
传入的是一个结构体,可以通过reflect.Type接口的方法获取结构体的字段个数、字段名称、字段类型等
可以访问任意值的类型信息
- Type(类型):包括的有自定义类型
- Kind(种类):指底层的类型
反射中像数组、切片、Map、指针等类型的变量,.Name()返回空
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "沙河小王子",
age: 18,
}
var e = book{title: "《跟小王子学Go语言》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
ValueOf
主要用于获取变量的值信息。可以通过reflect.Value接口的方法对值进行操作,如获取值、设置值、调用方法等
对于一个结构体变量,可以通过reflect.ValueOf获取其值,然后调用FieldByName等方法获取特定字段的值
可以和原始值之间相互转换
- Interface() 以 interface{} 类型返回
- Int() 以 int 类型,64位返回
- Uint() 以 uint 类型,64位返回
- Float() 以双精度(float64)类型返回
- bool() 以 bool 类型返回
- Bytes() 以字节数组 []byte类型返回
- String() 以字符串类型返回
var f float32
f=6.6
rval := reflect.ValueOf(f)
// f = rval.Float()
/* cannot use rval.Float()
(value of type float64) as float32 value in assignment */
f = float32(rval.Float())
fmt.Println(f)
应用
- 基本类型
import (
"fmt"
"reflect"
)
func test(b interface{}) {
// 通过反射获得变量的type,kind和值
//Type反射类型
rTyp := reflect.TypeOf(b)
fmt.Printf("rTyp的类型:%T,rTyp的值:%v \n",rTyp,rTyp)
// rTyp的类型:*reflect.rtype,rTyp的值:int
rVal := reflect.ValueOf(b)
fmt.Printf("rVal的类型:%T,rVal的值:%v \n",rVal,rVal)
/* rVal的类型:reflect.Value,rVal的值:100 */
/* cannot convert 2 (untyped int constant) to type struct{typ *reflect.rtype;
ptr unsafe.Pointer; reflect.flag} */
//n2 := 2+rVal
//fmt.Println(n2)
//返回值的有符号整数,不匹配的会panic
// panic: reflect: call of reflect.Value.Float on int Value
//n3 := rVal.Float()
//fmt.Println(n3)
//结构体类型只能通过断言后使用
n2 := 2+rVal.Int()
fmt.Println(n2)
// 102
//interface{}
iV :=rVal.Interface()
//转成对应类型
v := iV.(int)
fmt.Printf("v的类型:%T,v的值:%v \n",v,v)
// v的类型:int,v的值:100
}
func main() {
var num int = 100
test(num)
}
- 结构体类型
type Student struct {
Name string
Age int
}
func test2(b interface{}) {
rTyp := reflect.TypeOf(b)
rVal := reflect.ValueOf(b)
fmt.Println("rTyp=",rTyp," rVal=",rVal)
iV :=rVal.Interface()
fmt.Printf("iV的类型:%T,iV的值:%v \n",iV,iV)
/* rTyp= main.Student rVal= {张三 18}
iV的类型:main.Student,iV的值:{张三 18} */
//虽然类型和预期一样但是无法取出数据
// fmt.Println(iV.Name)
// iV.Name undefined (type any has no field or method Name)
//因为反射的本质是在运行时的,在运行时可以确定类型和值
//但是在编译时无法确定所以会报错
v := iV.(Student)
fmt.Println(v.Name)
// 张三
}
func main() {
stu :=Student {"张三",18}
test2(stu)
}
- kind
获取变量的类别,返回值是一个常量
//获取变量对应的kind
fmt.Println("rVal的kind=",rVal.Kind()," rTyp的kind=",rTyp.Kind())
//kind的范围比type大
// rVal的kind= struct rTyp的kind= struct
- Type和Kind可能相同,也可能不同
- 基本数据类型,两者是一样的
- 结构体类型不一样,Type:包名.Student,Kind:struct
Elem()
通过引用传递修改变量值,Elem()方法可以获取指针对应的值
- 通过反射修改变量,当使用SetXxx方法设置需要通过对应的指针类型来完成,从而改变变量值,同时使用reflect.Value.Elem()方法
import (
"fmt"
"reflect"
)
func test(b interface{}) {
rVal := reflect.ValueOf(b)
/* panic: reflect: reflect.Value.SetInt
using unaddressable value */
fmt.Println("rVal的kind:",rVal.Kind())
// rVal的kind: ptr
// rVal.SetInt(20)
//Elem返回v持有接口保管的值,或v持有指针指向的值的Value封装
//类似于指针获得变量地址后,指向该地址后修改变量
//elem()的返回值:{张三 18 66.6 女},elem()的类型:reflect.Value
rVal.Elem().SetInt(20)
}
func main() {
num := 66
//相当于值拷贝
// test(num)
test(&num)
fmt.Println(num)
// 20
str := "张三"
// fs := reflect.ValueOf(str) string类别
// reflect.Value.SetString using unaddressable value
// fs.SetString("周六")
fs := reflect.ValueOf(&str)
//注意必须通过指针进行修改
fs.Elem().SetString("周六")
fmt.Println(str)
// 周六
}
IsNil()和IsValid()
- IsNil()常被用于判断指针是否为空,v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一
- IsValid()常被用于判断返回值是否有效,分类必须是IsValid,String,Kind之中的方法
两个返回值都是bool类型
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}
结构体反射
- 使用反射遍历结构体字段,调用结构体的方法,并获取结构体标签的值
- Method通过传入参数获取该持有结构体对应的第几个方法(从0开始)
- Call通过该方法调用获取到的方法
StructField类型
用来描述结构体中的一个字段的信息
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}
import (
"fmt"
"reflect"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32
Sex string
}
func (s Monster) Print() {
fmt.Println("开始")
fmt.Println(s)
fmt.Println("结束")
}
func (s Monster) GetSum(n1,n2 int) int {
return n1+n2
}
func (s Monster) Set(name string,age int,score float32,sex string) {
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}
func TestStruct(a interface{}) {
typ := reflect.TypeOf(a)
val := reflect.ValueOf(a)
kd := val.Kind()
//reflect包下的常量
if kd != reflect.Struct{
fmt.Println("不是结构体")
return
}
num := val.NumField()
fmt.Println("结构体有",num,"个字段")
for i := 0; i < num; i++ {
//val.Field获取到变量里的值,但是不可以参与运算,需要转为对应的类型
fmt.Printf("第%d个字段,值=%v \n",i,val.Field(i))
//不可以使用val,因为val返回的是变量里的值,type返回的是StructField结构体
//里面Tag字段是StructTag结构体类型,里面的Get方法可以获取到变量里的值返回为string类型
tagVal :=typ.Field(i).Tag.Get("json")
if tagVal != ""{
fmt.Printf("第%d个字段,标签=%v \n",i,tagVal)
}
}
numOfMethod := val.NumMethod()
fmt.Println("结构体有",numOfMethod,"个方法")
//获取到第二个方法并执行,排序默认是按照函数名排序
//nil代表没有参数传入
val.Method(1).Call(nil)
//call中传入参数是reflect.Value切片
//调用结构体的第1个方法
var params []reflect.Value
//将变量转为reflect.Value类型进行操作
params = append(params,reflect.ValueOf(10))
params = append(params,reflect.ValueOf(40))
res := val.Method(0).Call(params)
//返回结果是[]reflect.Value
fmt.Println("res=",res[0].Int())
/* 结构体有 4 个字段
第0个字段,值=张三
第0个字段,标签=name
第1个字段,值=18
第1个字段,标签=monster_age
第2个字段,值=66.6
第3个字段,值=女
结构体有 3 个方法
开始
{张三 18 66.6 女}
结束
res= 50 */
}
func main() {
a := Monster{"张三",18,66.6,"女"}
//规范创建格式后就可以通过反射进行调用
//在使用时,创建一个实例后交给函数,不需要做其他工作
//在函数内部通过反射调用各个方法
TestStruct(a)
}
- 使用反射修改结构体字段的值,调用结构体方法
func TestStruct(a interface{}) {
val := reflect.ValueOf(a)
kd := val.Kind()
//获取到的参数类型是指针
if kd != reflect.Ptr && val.Elem().Kind() == reflect.Struct{
fmt.Println("不是结构体")
return
}
val.Elem().Field(0).SetString("周六")
val.Elem().Method(1).Call(nil)
}
func main() {
a := Monster{"张三",18,66.6,"女"}
//传入地址
TestStruct(&a)
fmt.Println(a)
/* 开始
{周六 18 66.6 女}
结束
{周六 18 66.6 女} */
}
网络编程
Tcp socket编程
主流网络编程,底层基于Tcp/ip协议
- 应用层(smtp,ftp,telnet,http)
- 传输层(解释数据)
- 网络层(定位ip地址和确定连接路径)
- 链路层(与硬件驱动对话)
IPv4:四个字节表示IP地址
IPv6:128位表示IP地址
端口:逻辑意义上的- 服务程序,必须监听一个端口
- 端口就是其他程序和该服务通讯的通道
- 一个IP地址有66535个端口
- 一个端口被某程序监听,那么其他程序就不能监听该端口
- netstat -an可以查看本机有哪些端口在监听
- netstat -anb查看监听端口的pid,结合任务管理器关闭不安全端口
服务器和客户端通信
import (
"fmt"
"net"
)
func process(conn net.Conn) {
//循环接收客户端数据
defer conn.Close()
for{
//创建一个新的切片
buf := make([]byte, 1024)
//客户端没有发送信息会一直等待,协程会阻塞在这里
// fmt.Println("服务器在等待客户端发送信息")
n,err := conn.Read(buf)
if err != nil{
//err是一个字符串
fmt.Println("接收失败",err)
//可能客户端已经关闭
return
}
/* 服务器在等待客户端发送信息
接收失败 read tcp 127.0.0.1:8888->127.0.0.1:64597: wsarecv:
An existing connection was forcibly closed by the remote host. */
//显示客户端发送内容到服务器终端
//在客户端发送时会将\n读入
//否则会将剩余的全部输出
fmt.Print(string(buf[:n]))
// helloworld,abc!
}
}
func main() {
fmt.Println("服务器开始监听")
//通过tcp协议监听
//127.0.0.1只支持IPv4
//0.0.0.0支持IPv4和IPv6
listen, err := net.Listen("tcp","0.0.0.0:8888")
if err != nil {
fmt.Println("监听失败",err)
return
}
//监听一下结束
fmt.Println("listen=",listen)
/* 服务器开始监听
listen= &{0xc000100a00 {<nil> 0}} */
//循环等待连接接口
for{
fmt.Println("等待客户端连接")
//返回连接
conn, err := listen.Accept()
if err != nil {
fmt.Println("当前连接失败",err)
}else{
fmt.Println("当前连接成功,conn=",conn)
// conn.RemoteAddr()返回的是Addr接口类型,调用里面的String方法获得IP字符串
fmt.Println("客户端IP=",conn.RemoteAddr().String())
// 客户端IP= 127.0.0.1:63486
//在主线程写接收,可能会发生阻塞
go process(conn)
}
//在终端使用telnet测试是否连接,ctrl+]终止,quit
//开启一个协程,为当前客户端服务
/* 当前连接成功,conn= &{{0xc000090280}}
等待客户端连接 */
}
// defer listen.Close()
}
输入exit退出客户端
import (
"fmt"
"net"
"bufio"
"os"
"strings"
)
func main() {
//连接协议,服务器IP+端口
conn ,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("客户端连接失败",err)
return
}
fmt.Println("连接成功,conn=",conn)
// 连接成功,conn= &{{0xc000100a00}}
//从控制台输入发送给服务器,在服务器接收显示
//接收键盘输入,可以一行行输入
// os.Stdin标准输入,一般代表键盘输入
//终端输入先写到一个文件中,在通过该文件读取到程序里
reader := bufio.NewReader(os.Stdin)
//从终端读取一行用户输入,并准备发送到服务器
//返回字符串
for{
line, err := reader.ReadString('\n')
if err != nil{
fmt.Println("读取失败",err)
}
//去除\r\n和空格
line = strings.Trim(line," \r\n")
if line == "exit"{
fmt.Println("客户端退出")
break
}
//发送
//参数是一个切片
// n,err := conn.Write([]byte(line))
_,err1 := conn.Write([]byte(line + "\n"))
if err1 != nil{
fmt.Println("当前发送失败",err1)
}
}
// fmt.Println("客户端发送",n,"字节的数据")
/* helloworld,abc!
客户端发送 17 字节的数据 */
}
TCP黏包
tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发,接收方不确定将要传输的数据包的大小
- Nagle算法造成的发送端的粘包,收到数据后并不立刻发送而是等待一段时间,在此期间有要发送的数据会一次发送两段数据
- 接收端接收不及时造成的接收端粘包,接收端不能及时读取数据,造成缓冲区存放多段数据
封包
给一段数据加包头,包头长度固定,并存储包体长度(过滤非法包时会加入包尾内容)
import (
"bufio"
"bytes"
//binary实现数字与字节序列之间的简单转换,以及变长整数(varint)的编码和解码
"encoding/binary"
)
// Encode 将消息编码
func Encode(message string) ([]byte, error) {
// 读取消息的长度,转换成int32类型(占4个字节)将包体的长度用四个字节表示
var length = int32(len(message))
//bytes.Buffer提供一个动态长的字节切片(字节缓冲区),实现了 io.Reader、io.Writer、io.ByteReader、io.ByteWriter、io.RuneReader 和 io.StringWriter 等接口
var pkg = new(bytes.Buffer)
//Write处理固定长度值的编码和解码
// 写入消息头(包头)
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
//Write(w io.Writer, order ByteOrder, data interface{}) error将data序列化成字节流,并写入到io.Writer中,data必须是固定长度的数据值或固定长度数据的切片
//LittleEndian表示小端字节序,BigEndian表示大端字节序
// 写入消息实(包体)
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
//Bytes()获取缓冲区的字节切片副本
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据(获取到包体的长度)
//将长度作为初始数据存入新的缓冲区
lengthBuff := bytes.NewBuffer(lengthByte)
//获取到前四个字节内容,转换为10进制保存
var length int32
//Read(r io.Reader, order ByteOrder, data interface{}) error 从io.Reader中读取字节流,并根据指定的字节序(ByteOrder)将字节流解码到data中
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。length表示包体的字节数,4表示包头的字节数
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
//包含的有4个字节的包头和剩下的包体
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
import (
"bytes"
"encoding/binary"
"fmt"
)
func main() {
str := "hello中国"
len := int32(len(str))
fmt.Println("字符串的字节数:", len)
var pkg = new(bytes.Buffer)
binary.Write(pkg, binary.LittleEndian, len)
fmt.Printf("pkg的类型:%T\n", pkg)
fmt.Println("byte切片的值:", pkg.Bytes())
var len1 int32
binary.Read(pkg, binary.LittleEndian, &len1)
fmt.Println("字符串的字节数:", len1)
/*字符串的字节数: 11
pkg的类型:*bytes.Buffer
byte切片的值: [11 0 0 0]
字符串的字节数: 11*/
}
UDP通信
用户数据报协议,是OSI参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的,没有时序的通信
//服务端
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
//监听到请求就可以进行数据传输,不需要再次建立连接
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
//客户端
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
b/s http编程
使用浏览器访问服务器时,使用http协议,http底层用tcp socket实现
设置运行的CPU数量
import (
"fmt"
"runtime"
)
func main() {
//获取当前系统CPU数量
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=",cpuNum)
//设置使用多少CPU
//在1.8后不需要手动设置
runtime.GOMAXPROCS(cpuNum-1)
fmt.Println("ok")
}