安装配置
官方cn镜像下载 + 下载GoLand(或是VSCode)
配置GOPATH,配置存放代码的路径(该文件夹下包含bin、pkg、src)
配置path,新增上面的bin路径
Goland配置 先配置sdk
配置
命令行
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go原生指令
go run 编译并执行,后面接 .go 文件。
go build 编译生成可执行文件。(可以编译跨平台的)
基础语法
go语言是面向接口编程
package main说明编译成可执行文件,该文件必须包含main函数作为程序入口。
package main //起始必须为main包下
import "fmt"
func main(){ //起始方法为main方法
fmt.Println("Hello World!")
}
变量和常量
输出格式
%d十进制 %o八进制 %x十六进制 %b二进制 %T输出变量类型 %v变量值 %p地址
基本数据类型:
整型(int8、int16、int32、int64、默认int和操作系统位数有关)、浮点型(float32、默认float64)、复数(complex关键字) 、布尔值bool(默认false) 、字符串类型(原生类型,utf-8)
字符类型:使用 英文byte 中文int32 注:应使用单引号
强制类型转换:新类型(变量)
判断基本类型变量的类型:
func main() {
var b bool
_, ok := interface{}(b).(bool)
fmt.Println(ok)
}
变量声明
声明变量:var 变量名 变量类型
批量声明:
var(
a string //""
b int //0
isOk bool //false
f float32 //
)
变量声明之后必须使用,否则报错
变量声明并赋值var a int =100
类型推导var s1= "hello"
短变量声明(只能在函数中使用)s3 := ”hello“
匿名变量 _
多用于占位
常量const和iota
常量在定义时就必须初始化,并不能在后续代码中 修改
语法const pt=3.14
将var 换成const
const(
a=iota
b
c
d,e=iota,iota
)//iota会从0开始,按行递增
数组
var 数组名[长度] 类型
var a [10]char;
初始化:默认false,0,“”
方式1
var a1 [3]bool;
a1=[3]bool{true,true,true}
方式2
a2:=[10]int{xx,xx,xx};
a3:=[]int{xx,x,xx}
数组复制 数组a=b ,是把b深拷贝一份给a
输出数组名的时候会输出 数组中所有元素值
a1:=[10]int{1,2,3,4,5,6,7,8,9}
fmt.Println(a1)
字符串
字符类型:
uint8类型,或者byte类型,代表了ASCII码的一个字符
rune(int32)类型,代表UTF-8字符
中文英文字符 int32
字符串修改
先转rune切片,分解为单个字符。
strings静态方法
str:="hello"
len(str)
strings.Split(str,char)//hello 用 e 切分成h llo,e会被舍弃
strings.Contains(str,"h")//true
strings.Index(s,str);//返回str第一次在s中出现的下标
strings.Join(s,"|");//s为字符串数组,在s每个字符串之间加| ,最后拼接成一个字符串
循环
for i:=0;i<100;i++{
xxx;
}
s:="hello";
for _,c:=range s{
xxx;
}//从字符串中拿到字符\
for i,v:=range a{
xxx;
}//数组遍历
流程控制
循环,只有for循环
for i:=0;i<100;i++{
xxx;
}
s:="hello";
for _,c:=range s{
xxx;
}//从字符串中拿到字符
//for range循环
s:="hello";
for i,v:=range s{
fmt.Printf("%d %c"i,v)
}
s:="你好"
for _,c:=range s{
fmt.Printf("%c\n",c)
}
(如果有中文,中文字符索引会间隔3个字节;换言之中文字符串,每个字符下标间隔为3)
分支if-else,同上条件处不加括号。
切片 slice
数组的长度是固定的
切片是拥有相同元素的可变长度的序列。基于数组类型做的一层封装,支持自动扩容。
切片是引用类型,指向底层数组,内部包含地址、长度、容量
创建语法:
s :=[] int {1,2,3 }
s := arr[:]
s := arr[startIndex:]
s := arr[:endIndex]
s1 := s[startIndex:endIndex]
s :=make([]int,len,cap)
切片默认为空 nil
切片会对应一个底层数组,切片只是对应这个数组的一个局部框
通过数组得到切片
s3:=a1[0:4] //左闭右开
这样的切片长度看自己,容量看切片位置开始到原数组最后
底层数组更改,切片也会改
切片的赋值拷贝(浅拷贝)
s1=s2,指向同一个地址,一改全改
常用方法:
切片长度len(切片名) 切片容量cap(切片名)。
判断切片是否为空:len是否等于0
直接创建切片 make(类型,长度,容量)
;容量省略则等于长度。例:s1:=make([]int,5,10)
数组创建切片:
arr := [8]int{}
mySlice := arr[4:6] //越界会抛出panic
//此时的len == 2,cap == 4(下标从4到7),值为0 0
//用数组复制给到切片的时候,若复制的数组元素后面还有内容的话,则后面的内容都作为切片的预留内存
append,追加元素。涉及到扩容问题(扩容策略)。底层数组地址可能会变化。
s1=append(s1,ss…)//ss…表示拆分ss
copy,复制切片,copy(源,目标)
sort,排序,sort.Ints(切片变量)
for i,v range 切片 只会遍历到len为止,不会到cap
切片扩容
- 如果原来的切片容量小于1024
那么新的切片容量就会扩展成原来的 2 倍
- 如果原切片容量大于等于1024
那么新的切片容量就会扩展成为原来的1.25倍
指针
只有&取地址,*取值,不能直接操作指针。
new函数:申请一个内存地址
var a=new (int);
*a=100;
make函数:只用于slice、map、chan的内存创建
map
内部是hash,k-v,无序不重复
语法:var xxx map [keytype]valueType
var m1 map[string]int;//这样没有初始化
分配内存:m1=make(map[string]int,10);
//分配10个容量
合并 创建并分配内存:m1:=make(map[string]int,10)
添加元素:m1[“hello”]=100;
常用方法
获取返回值:
s:=m1[“zhangsan”];//如果没有值,就返回对应类型零值。
v,ok:=m1[''zhangsan"];//有值就v返回值,ok返回true;无值ok返回false
遍历:for k,v:=range m1{xxxxx};
删除:delete(map变量,key)
delete(m1,“zhangsan”);
与切片组合使用:
切片里面是map
var s1=make([]map[int]string,10,10)//这时只分配了切片的内存,没有分配map的
s1[0]=make(map[int]string,10);//分配map内存
s1[0][0]="zhangsan";//存放元素
map里面是切片
var m1=make(map[string][]int,10);
m1["beijing"]=[]int{10,20,30};
函数
go语言没有方法重载
//返回值列表可以省略
func 函数名(参数)(返回变量名 返回值类型){
xxx
}
func f1(x int,y int){...}
func f2(){.....}
func f3()int{....}//返回值类型为int
func f4(x int,y int)(ret int){.....}//返回值ret为int,在函数里可以直接使用ret,return的时候也可以只写return
func f5()(int,string){.....}//可以多个返回值
func f6(x,y int)int{....}//参数简写
func f7(x int,y ...int)int{....}//可变长参数
函数类型
函数类型 func()、func()int、func(int,int)int,括号里是形参类型,括号后是返回值类型
函数也可以作为参数传递给函数
func f(x func()int){//这里的int是指传入参数的这个函数返回值是int,传入参数的函数没有其自身的参数列表
ret=x();
fmt.Println(ret);
}
函数还可以作为返回值
func f(x func()int)func(int,int)int{
ret:=func(a,b int)int{
return a+b;
}
return ret;
}
立即执行函数
在函数体之后加括号,无返回值。如果有参数,则实参写在函数体之后的括号中。
func(x int, y int){
xxx
}(10,20)
defer
defer语句会将后面跟随的语句进行延迟处理。延迟到函数即将返回的时候执行。多个defer,用的栈结构存储。先进后出。
defer多用于释放连接。
return不是原子操作,第一步返回值复制,第二步返回。defer执行在这两步之间。
闭包
闭包是一个函数,这个函数包含了他外部作用域的一个变量。
func f1(f func()){
xxx;
}
func f2(x,y int){
xxx;
}
func main(){
f1(f2);//错误f2()类型和f1()所需参数类型不一致
}
修改
func f1(f func()){
fmt.Println("f1");
}
func f2(x,y int){
fmt.Println("f2");
}
//定义一个函数f3对f2包装,f1的入参是f3返回值,f2是f3入参
func f3(f func(int ,int))func(){
f(10,20);
return func(){
fmt.Println("f3");;
}
}
func main(){
f1(f3(f2));
}
结构体
type关键字
type MyInt int//自定义类型,在编译完成之后显示的类型是 main.MyInt,表示main包下MyInt类型
type myInt2 =int//类型别名,编译完成之后还是int类型
结构体语法
type 类型名 struct{
字段名 类型
字段名 类型
......
}
匿名结构体
var s struct{
字段名 类型
字段名 类型
......
}//声明了s是个结构体变量,包含这些字段属性。一般用于临时场景
结构体也是new开辟内存,但new不能初始化赋值
var p=new(person)//p是person指针类型
通过指针对结构体变量取值,也是用 p.name
; //语法糖
初始化结构体
var p2:=person{
name: "zs",
age:18,
}//赋值用的冒号
p4:=person{
"ls",
20,
}//使用值列表初始化
结构体内存布局:连续的,也有内存对齐机制
结构体嵌套
直接嵌套 相当于是深拷贝一份副本进去
type aStruct struct {
age int
name string
}
type bStruct struct {
a aStruct
extra string
}
func main() {
a := aStruct{
20,
"kramer",
}
b := bStruct{
a,
"extra",
}
b.a.age++
fmt.Println(a.age) //20
fmt.Println(b.a.age) //21
}
模拟构造方法
返回值可以返回person类型,或者结构体指针。结构体比较大的时候用指针。减少程序内存开销。
构造函数约定newXxxx格式
go语言没有方法重载
func newPerson(name string,age int)person{
return person{
name:name,
age:age,
}
}
方法
作用于特定变量的函数,和函数相比前面多了接收者
语法:
func (接收者 接收者类型) 函数名 (参数列表) (返回值列表){
xxx 函数体
}
func (p person)sayHello(){
xxx;
}//接收者p表示的是调用该方法的具体类型变量
Go语言约定,首字母大写的方法表示对外部包可见的。
接收者也可以是指针类型,这样才能修改接收者的内部属性,用原类型只能修改副本。
只能给自己的定义的类型添加方法。
结构体匿名字段
不常用
type person strut{
string
int
}
func main(){
p1:={
"zs"
10
}
fmt.Println(p1.string)
}
//存在问题,相同类型只能写一个
结构体嵌套
匿名嵌套,可以直接p.xxx, xxx是嵌套内的结构体的变量名
他会先在自己的结构体中找,找不到去匿名的里面找。
模拟继承
在go语言中是没有继承的,只能模拟。通过结构体匿名嵌套继承(包括属性和方法)
type animal struct{
name string
}
func (a animal)move(){
fmt.Printlf(a.name,"move")
}
//狗类
type dog struct{
feet uint8
animal//匿名结构体嵌套
}
//给狗实现汪汪
func (d dog)wang(){
fmt.Printlf("%s汪汪汪",d.name)
}
func main(){
d1:=dog{
animal:animal{
name:"旺财"
},
feet:4,
}
fmt.Println(d1)
}
结构体和JSON
1、结构体转JSON:序列化
2、JSON转结构体:反序列化
注意大写,在别的包里用需要大写
package main
import (
"encoding/json"
"fmt"
)
type person struct{
Name string `json:"name"`//json
Age int
}
func main(){
p1:=person{
Name:"zs",
Age:20,
}
b,error:=json.Marshal(p1)
if error!=nil{
fmt.Println("Marshal failed")
return
}
fmt.Println(string(b))
//反序列化
str:="{\"name\":\"ls\",\"age\":22}"
var p2 person
json.Unmarshal([]byte(str),&p2);//中文也是byte数组
fmt.Println(p2)
}
接口
接口是一种特殊类型,(基本类型,结构体类型…)
规定了变量有哪些方法
用来作为变量、参数、返回值的类型
语法:
type 接口名 interface{
方法1(参数列表)返回列表;
方法2(参数列表)返回列表;
......
}
只要实现所以接口方法,即可视为此接口类型变量
package main
import "fmt"
type speaker interface{
speak ()
}
type cat struct{}
type dog struct{}
func (c cat)speak(){
fmt.Println("mmm")
}
func (d dog)speak(){
fmt.Println("www")
}
func da(s speaker){
s.speak()
}
func main(){
var c cat
var d dog
da(c)
da(d)//test
}
值接收者和指针接收者实现接口的区别
区别:
使用值接收者,接口变量既能存值也能存指针
使用指针接收者,接口变量只能传存指针(主要)
1、值接收者
package main
import "fmt"
type speaker interface{
speak ()
}
type cat struct{
name string
age int
}
//使用值接接收者实现接口方法
func (c cat)speak(){
fmt.Println("mmm")
}
func main(){
var s speaker
c1 :=cat{"zs",20}//cat类型
c2 :=&cat{"ls",22} //*cat 类型
s=c1
fmt.Println(s)//输出:{zs,20}
s=c2 //没问题 可以存
fmt.Println(s)//输出:&{ls,20}
}
2、指针接收者
package main
import "fmt"
type speaker interface{
speak ()
}
type cat struct{
name string
age int
}
//使用指针接接收者实现接口方法
func (c *cat)speak(){
fmt.Println("mmm")
}
func main(){
var s speaker
c1 :=cat{"zs",20}//cat类型
c2 :=&cat{"ls",22} //*cat 类型
s=c1
s.speak() //报错,实现speaker接口的是*cat 类型
s=c2
s.speak() //输出mmm
}
接口和类型关系
同一个结构体可以实现多个接口,接口还可以嵌套
package main
type animal interface{
mover
eater
}
type mover interface{
move()
}
type eater interface{
eat()
}//test
空接口(常用)
可能接受多种变量类型就用空接口
任意类型都实现了空接口,也就是任意类型的变量都能保存到空接口中’
作用:通常作为函数的参数或者map value的类型
type xx interface{
}
//空接口没必要起名字,通常为以下格式
interface{}
使用实例
package main
func main(){
var m1 map[string]interface{}
m1=make(map[string]interface{},16)
//test 合并
m1:=make(map[string]interface{},16)
}
类型断言
解决问题:使用空接口之后怎么判断类型
语法:x.(T)
x表示类型为interface{}的变量,T表示x可能的类型
返回两个参数:1、x转化为T类型后的变量。2、布尔值,若为true则表示断言成功,false表示失败
语法:x.(type)
获取x类型
package main
func assign(a interface{}){
str,ok:=a.(string)
if ok{
fmt.Println(str)//test 返回值
}
}
func main(){
assign(100)
}
改写直接拿到类型:
package main
func assign(a interface{}){
switch a.(type){
case string:
case bool:
}
}
func main(){
assign(100)
}
包 package
导入
从src下路径开始
只有main包才能编译成可执行文件
包中标识符(变量名、函数名、结构体、接口等)首字母小写表示私有,首字母大写表示可被外部包调用
目录名最好和包一致
取别名
路径为GOPATH/src/code/gocode/xxx(目录名)
别名为xxx
import{
xxx "code/gocode/xxx"
}
go语言禁止循环导入
匿名导包:导入包名前加下划线
(导包自动触发包内init()函数,init()函数,没有参数也没有返回值。init()自动执行不能主动执行)
一个包只能有一个init()
import{
_ "code/gocode/xxx"
}
文件
读
打开文件
package main
import "os"
func main(){
fileObj,err:=os.Open("./main.go")//可以绝对路径也可以相对路径。./表示当前路径
if err != nil{
fmt.Println("open failed:%v",err)
return
}
fmt.Println("open success")
defer fileObj.close()
}
读文件
Read方法func (f *File)Read(b []byte)(n int,err error)
n表示读了多少字节
package main
import "os"
func main(){
fileObj,err:=os.Open("./")//可以绝对路径也可以相对路径。./表示当前路径下
if err != nil{
fmt.Println("open failed:%v",err)
return
}
fmt.Println("open success")
defer fileObj.close()
//读文件
var tmp=make([]byte,128)//指定读的长度
n,err:=fileObj.Read(tmp)//n表示读了多少字节
if err != nil{
fmt.Println("open failed:%v",err)
return
}
fmt.Println("读取了:",n)
fmt.Println(string(tmp[:n]))
}
bufio读取文件
bufio是在file基础上封装了一层API,支持更多的功能。
先创建reader对象,然后再读
package main
import {
"bufio"
"fmt"
"io"
"os"
}
func mian(){
file,err:=os.Open("./main.go")
if err!=nil{
fmt.Println("open failed,error:",err)
return
}
defer file.Close()
reader :=bufio.NewReader(file)//创建reader对象
for{
line,err:=reader.ReadString(\n')//注意是字符
if err == io.EOF{
fmt.Println("文件读取完成")
}
if err!=nil{
fmt.Println("open failed,error:",err)
return
}
fmt.Print(line)
}
}
ioutil读文件(简单)
func readFileByIoutil{
ret,err:=ioutil.ReadFile("./main.go")
if err!=nil{
fmt.Println("open failed,error:",err)
return
}
fmt.Println(ret)
}
写
os.OpeFile()函数能以指定方式打开文件,从而实现文件写入相关功能
func OpenFile(name string,flag int,perm FileMode)(*File,error){...}
name 要打开的文件名,perm 表示权限(Linux下使用,windows一般不用),flag打开文件的模式。有以下几种:
模式 | 含义 |
---|---|
os.O_WRONLY | 只写 |
os.O_CREATE | 创建文件 |
os.O_RDONLY | 只读 |
os.O_RDWR | 读写 |
os.O_TRUNC | 清空 |
os.O_APPEND | 续写 |
package main
import (
"fmt"
"os"
)
//打开文件写内容
func main(){
fileObj,err:=os.OpenFile("./xx.txt",os.O_APPEND|os.O_CREATE,0644)//创建并续写,底层使用二机制位来表示打开模式
if err!=nil{
fmt.Println("open failed,error:",err)
return
}
// Write和WriteString
fileObj.Write([]byte("zhangsan "))
fileObj.WriteString("lisi ")
fileObj.Close();
}
bufio.NewWriter
先写入缓存,再写入文件
package main
import (
"fmt"
"os"
)
//打开文件写内容
func main(){
fileObj,err:=os.OpenFile("./xx.txt",os.O_APPEND|os.O_CREATE,0644)//创建并续写,底层使用二机制位来表示打开模式
if err!=nil{
fmt.Println("open failed,error:",err)
return
}
defer fileObj.Close()
//创建写对象
wr:=bufio.NewWriter(fileObj)
wr.WtireString("wangwu ")//写到缓存
wr.Flush()//将缓存中的内容写入文件
}
ioutil.WriteFile
package main
import (
"fmt"
"os"
)
//打开文件写内容
func main(){
str:="hello 张三"
err:=ioutil.WriteFile("./xx.txt",[]byte(str),0666)//会把原有内容删除
if err!=nil{
fmt.Println("write file failed,err:",err)
return
}
}
直接往文件写
一般用在写日志
func main(){
fmt.Fprintln(os.Stdout,"这是一条日志记录!")//写到控制台
fileObj,_:=os.OpenFile("./xxx.log",os_OCREATE|os.O_APPEND|os.WRONLY,0955)
fmt.Fprintln(fileObj,"这是一条日志记录!")//写到日志文件
}
反射
基本介绍:
1、反射可以在运行时动态获取变量的各类信息,比如变量的类型、类别
2、如果是结构体变量,还可以获得结构体本身的信息(结构体字段、方法)
3、通过反射可以修改变量的值,可以调用关联方法
4、使用反射需要import “reflect”
库
time包
time.Time类型表示时间。通过time.Now()方法获取当前时间,然后获取时间对象的年月日时分秒等信息
Duration 时间间隔类型,
func main(){
now :=time.Now()
fmt.Println(now)
year:=now.Year()
month:=now.Month()
day:=now.Day()
hour:=now.hour()
minute:=now.Minute()
second:=now.Second()
fmt.Println()
//获取时间戳
now.Unix()//秒
now.UnixNano()//纳秒
//时间戳转化成日期
time.Unix(1564803777,1)
//now + 1 hour
now.Add(1 * time.Hour)
//定时器
timer:=time.Tick(time.Second)
for t:=range timer{
fmt.Println(t)//一秒钟执行一次
}
}
时间格式化
按照2006 1 2 3 4 5格式
func main(){
now :=time.Now()
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("2006/01/02/15/04/05"))//24小时制
fmt.Println(now.Format("2006/01/02/03/04/05"))//12小时制
fmt.Println(now.Format("2006/01/02 15:04:05.000"))//精确到毫秒
}
把一个字符串时间转换成时间戳
func main(){
//按照对应格式解析时间
timeObj,err:=time.Parse("2006-01-02","1997-09-08")
if err!=nil{
fmt.Println("输入时间格式有误")
return
}
fmt.Println(timeObj)
fmt.Println(timeObj.Unix())
}
Sleep()
作用:让出cpu
参数:Duration类型
func main(){
n:=100
time.Sleep(time.Duration(n))//用变量传需要显式转换类型
time.Sleep(100)//隐式转换类型
}
reflect包
在go语言反射机制中,任何接口值都是由一个具体类型和具体类型的值两部分组成的。在go语言中反射相关功能由reflect包提供,任意接口值在反射中都可以理解成由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf(变量)
和reflcet.ValueOf(变量)
两个函数来获取任意对象的Type和Value
注意 反射中 变量、interface{}、reflect.Value是可以相互转换的
变量-(传递interface{}类型参数)->空接口-(reflect.ValueOf()函数)->reflect.Value
变量<-(类型断言)-空接口<-(v.Interface()//v类型为reflext.Value)-reflect.Value
func test(b interface{}){
//1、interface{}转reflect.Value
rVal := reflect.ValueOf(b)
//2、reflect.Value转interface{}
iVal :=rVal.Interface()
//3、interface{}转原来的数据类型
v:=iVal.(Student)
}
反射缺点:
1、反射代码极其脆弱,反射中类型错误会在真正运行的时候才会引发panic,那很可能导致在代码写完的很长时间之后
2、大量使用反射的代码难阅读难理解
3、反射的性能低下哎,基于反射的代码通常比正常代码慢一到两个数量级
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}){
t:=reflect.TypeOf(x)
fmt.Println(t)
v:=reflect.ValueOf(x)//这里拿到的值是实际类型为reflect.Value的值
fmt.Println(v)
}
func main(){
var a float64=3.14
reflectType(a)
var b int64=3
reflectType(b)
}
type name和type kind
在反射中关于类型还划分为:类型Type和种类Kind(Kind是一个常量)。kind的范畴更大,即粒度更粗;type粒度更精细。
reflect.TypeOf()拿到的是具体的详细的类,例如自定义的main.cat
package main
import (
"fmt"
"reflect"
)
type cat struct {
Name string
Age int
}
func reflectType(x interface{}){
t:=reflect.TypeOf(x)
fmt.Println(t.Name())//cat
fmt.Println(t.Kind())//strut
//reflect.Value(t).Kind()获取变量的类别,是一个常量。
//TypeOf和ValueOf 再Kind(),拿到的是同样的
v:=reflect.ValueOf(x)//转成reflect.Value类型
iv:=v.Interface()//转成空接口类型
fmt.Printf("iv=%v iv type=%T\n",iv,iv)//输出:iv={tom 20} iv type=main.cat
//将interface{}使用断言转回cat类型,从而才能取出成员变量
//fmt.Printf("iv=%v iv type=%T name==%v\n",iv,iv,iv.Name)//编译报错。在运行的时候知道是cat类型,但是编译发生在运行之前,也就是编译时无法知道类型
myCat,ok:=iv.(cat)
if ok{
fmt.Printf("myCat.Name=%v\n",myCat.Name)//输出:myCat.Name=tom
}
}
func main(){
var c=cat{"tom",20}
reflectType(c)
}
通过反射取值
func main(){
a:= 10
rVal:=reflect.ValueOf(a)
n1:=rVal.Int()
//n1:=rVal.Float()//编译通过,运行时panic: reflect: call of reflect.Value.Float on int Value
fmt.Println(n1)
}
重点:通过反射设置变量的值
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须通过传递变量地址才能修改变量的值。而反射中使用专有的Elem()
方法来获取指针对应的值
package main
import (
"fmt"
"reflect"
)
func reflectsetValue1(x interface{}){
v:=reflect.ValueOf(x)
if v.Kind()==reflect.Int64{
v.SetInt(200)
}
}
func reflectsetValue2(x interface{}){
v:=reflect.ValueOf(x)
//传入地址时,v是一个地址
//fmt.Println(v.Kind())//ptr
//反射中使用Elem()方法获取指针对应值的Value封装
if v.Elem().Kind()==reflect.Int64{
v.Elem().SetInt(200)//SetXxx()方法绑定的是(v Value)来调用
}
}
func main(){
var a int64=100
//reflectsetValue1(a)//出错
reflectsetValue2(&a)//修改变量值必须传地址
fmt.Println(a)
}
通过反射获取结构体字段的值
package main
import (
"fmt"
"reflect"
)
type student struct {
Name string `json:"name"`
Score int `json:"score"`
}
func main(){
stu:=student{"zs",99}
t:=reflect.TypeOf(stu)
fmt.Println(t.Name(),t.Kind())
//遍历结构体所有字段信息
fmt.Println(t.NumField())//2
for i:=0;i<t.NumField();i++{
field:=t.Field(i)
fmt.Printf("name:%s index:%d type:%v json_tag:%v \n",field.Name,field.Index,field.Type,field.Tag.Get("json"))
}
//通过字段名获取指定结构体字段信息
if scoreField,ok:=t.FieldByName("Score");ok{
fmt.Printf("name:%s index:%d type:%v json_tag:%v \n",scoreField.Name,scoreField.Index,scoreField.Type,scoreField.Tag.Get("json"))
}
}
stoconv标准库
把字符串转化成整数
package main
func main(){
i:=int32(64)
fmt.Println(string(i))//输出的是'@'而不是“64”
ret:=fmt.Sprintf("%d",i)//数字转化字符串
fmt.Println(ret)//输出的是“64”
ret,_:=strconv.ParseInt("1000",10,64)//字符串转10进制int64,第二个参数是进制,三个参数是int64
fmt.Println(ret)//输出 1000
//字符串转整形
ret2,_:=strconv.Atoi("1000")
fmt.Println(ret2)//输出 1000
//整形转字符串
ret3:=strconv.Itoa(88)
fmt.Println(ret3)//输出 88
//从字符串中解析布尔值
boolStr:="true"
boolValue,_:=strconv.ParseBool(boolStr)
fmt.Println(boolValue)//输出 true
}
零散
输入
func main(){
var s string
fmt.Scanln(&s)//读到空白符(空格 回车)就结束
}
func useBufio(){
var s string
reader :=bufio.NewReader(os.Stdin)
s,_=reader.ReadString('\n')// 读到换行符才停止
fmt.Println(s)
}
随机数
rand.Seed(time.Now().UnixNano());//加种子可以让每次执行程序的时候随机数不同
rand.Intn(100);//生成0到99整数
并发
并发:同时间段执行多个任务
并行:同一时刻执行多个任务
go语言的并发通过goroutine
实现的,goroutine
类似线程,属于用户态的线程,goroutine
是go语言运行时调度完成,而线程是操作系统调度完成。go语言还提供channel
在多个goroutine
中通信。go程序会智能地将goroutine
分配到各个CPU,在语言层面内置了调度和上下文切换机制。
当需要某个任务并发执行的时候,只需要把这个任务包装成一个函数,开启一个goroutine
去执行这个函数就可以了。
知识点
goroutine和线程的关系:OS线程一般都有固定的栈内存,goroutine的栈在其生命周期开始时只有很小的栈(2KB)goroutine的栈不是固定的,它可以按需增大或缩小,大小限制可以到1GB。go语言中一次可以创建十万左右的goroutine。
goroutine调度
GMP时go语言运行时层面的实现,是go语言自己实现的一套调度系统,区别于操作系统的OS线程
G:就是个goroutine,里面除了goroutine信息外,还有与所在P绑定等信息
M:是Go运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是一一映射的关系,一个goroutine最终是要放到M上执行的
P:管理一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间长的goroutine暂停、运行后续的goroutine等)当自己的队列消费完了之后就去全局队列里取,如果全局也消费完了,就去其他P队列中抢任务
P和M一般也是一一对应的,P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时回收旧的M
P个数是通过runtime.GOMAXPROCS
设定(最大256),Go1.5之后默认为物理线程数。在并发量大的时候会增加一些P和M,但也不会太多,切换频繁得不偿失
单从线程调度讲,Go语言相比其他语言优势在于OS线程是由OS内核来调度,goroutine则是Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调用m个goroutine到n个OS线程)。其一大特点时goroutine的调度是在用户态完成的,不涉及内核态和用户态的频繁切换,包括内存的分配和释放,都是在用户态下维护着一块大的内存池,不直接调用malloc函数,成本比调度OS线程低很多。另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,再加上本身goroutine的超轻量,保证了go调度方面的性能
实现并发
语法:
在所调用函数前面加上go关键字,就可以为函数创建一个goroutine
程序启动之后,会创建一个主goroutine去执行。main函数结束之后,由main函数启动地goroutine也结束了。
func hello(i int){
fmt.Println("hello",i)
}
func main(){
for i:=0;i<100;i++{
go hello(i)
}
fmt.Println("main")
time.Sleep(time.Second)
}//输出:会是乱序的
使用匿名函数
func main(){
for i:=0;i<100;i++{
go func(){
fmt.Println(i)
}()
}
fmt.Println("main")
time.Sleep(time.Second)
}//输出:会缺失部分i,也会输出相同的i,因为输出的i是从函数外部拿的
解决
func main(){
for i:=0;i<100;i++{
go func(i int){
fmt.Println(i)
}()
}
fmt.Println("main")
time.Sleep(time.Second)
}//输出:无重复,但乱序
重点:同步
使用sync.WaitGroup
来实现goroutine的同步
例1:
var wg sync.WaitGroup
func hello(i int){
defer wg.Done()//goroutine结束就登记-1
fmt.Println("hello Goroutine",i)
}
func main(){
for i:=0;i<10;i++{
wg.Add(1)//启动一个goroutine就登记+1
go hello(i)
}
//time.Sleep(3)//时间不够就可能有的goroutine执行不到
wg.Wait()//等待所有登记的goroutine结束
}
1、多次执行上述代码,发现每次打印顺序不是一致的。这是因为十个goroutine是并发执行的,而goroutine的调度是随机的。
2、goroutine什么时候结束:goroutine对应函数结束了,goroutine就结束了。
例2:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func f(i int){
time.Sleep(time.Millisecond*time.Duration(rand.Intn(400)))
fmt.Println("f:",i)
wg.Done()
}
func main(){
for i:=0;i<10;i++{
wg.Add(1)
go f(i)
}
//如何知道十个goroutine都执行完毕
wg.Wait()
}
也可以把var wg sync.WaitGroup
定义在main函数中,需要注意使用指针
func f(i int,wg *sync.WaitGroup){
time.Sleep(time.Millisecond*time.Duration(rand.Intn(400)))
fmt.Println("f:",i)
wg.Done()
}
func main(){
var wg sync.WaitGroup
for i:=0;i<10;i++{
wg.Add(1)
go f(i,&wg)
}
//如何知道十个goroutine都执行完毕
wg.Wait()
}
runtime.GOMAXPROCS
默认是runtime.GOMAXPROCS等于CPU的核心数
当runtime.GOMAXPROCS设置为1时,会先打印a或者b然后打印另一个。
当runtime.GOMAXPROCS设置大于1时,有大概率交替输出a和b
package main
import (
"fmt"
"runtime"
"sync"
)
func a(){
defer wg.Done()
for i:=0;i<10;i++{
fmt.Printf("A:%d\n",i)
}
}
func b(){
defer wg.Done()
for i:=0;i<10;i++{
fmt.Printf("B:%d\n",i)
}
}
var wg sync.WaitGroup
func main(){
runtime.GOMAXPROCS(1)//只有一个OS线程
wg.Add(2)
go a()
go b()
wg.Wait()
}
channel
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
特点:
1、本质是队列的数据结构
2、channel是线程安全的
3、channel是有类型的,string类型的channel只能放string
4、必须初始化才能写入数据,即make之后才能使用
同一线程内,超容量存,或者取空channel都会阻塞,导致死锁
ch:=make(chan int,1)//ch存放的管道的地址
ch<-100
//ch<-101//导致死锁
<-ch
//<-ch//ch中无元素直接取,会导致死锁
length:=len(ch)//长度
capacity:=cap(ch)//容量
不同线程公用的channel,超容量存,空channel取只会导致goroutine阻塞
声明通道类型的语法:var 变量名 chan 类型
chan是引用类型,需要初始化
package main
var ch chan int
func main(){
fmt.Println(ch)//会输出nil
//通道初始化,通道必须make初始化才能使用
ch=make(chan int)//后面可以跟一个容量 b=make(chan int,16)带缓冲区的通道初始化,超出容量放不下,代码会报错
//发送到通道
ch<-10//把10发送到通道中,报错 只有一个goroutine
//接受
x:=<-ch
//还可以使用 for range拿通道里的值
//关闭通道
close(ch)
}
注:channel是队列数据结构,先进先出
package main
import (
"fmt"
"time"
)
//生产者
func producer(ch chan<- int){
i:=0
for{
ch<-i
i++
}
}
//消费者
func customer(ch <-chan int){
for{
num:=<-ch
fmt.Println(num)
}
}
func main(){
var ch=make(chan int,100)
go producer(ch)
go customer(ch)
time.Sleep(time.Millisecond)
}
单向通道
让调用者只能发送数据
//这个函数只能往ch1传入数据
func f1(ch1 chan<- int){
}
//这个函数只能从ch1拿出数据
func f2(ch1 <-chan int){
}
任意类型管道
Cat定义省略
func main(){
allChan:=make(chan interface{},10)
allChan<-Cat("cat",4)
newCat:=<-allChan
//fmt.Printf("newCat.Name=%v",newCat.Name)//编译不能通过
//类型断言
a:=newCat.(cat)
fmt.Printf("a.Name=%v",a.Name)
管道关闭
管道关闭之后,只能读 不能写
intChan:=make(chan int,10)
close(intChan)//关闭(双向/只发送)管道,应该由发送者执行
管道遍历
channel 使用for遍历会出现:
channel支持for range的方式进行遍历:
注意遍历时。如果channel没有关闭,会出现deadlock错误
通道的异常
channel | nil | 非空 | 空的 | 满了 | 没满 |
---|---|---|---|---|---|
接收 | 阻塞 | 接收值 | 阻塞 | 接受值 | 接受值 |
发送 | 阻塞 | 发送值 | 发送值 | 阻塞 | 发送值 |
关闭 | panic | 关闭成功,读完数据后返回零值 | 关闭成功,返回零值 | 关闭成功,读完数据后返回零值 | 关闭成功,读完数据后返回零值 |
关闭已经关闭的channel也会引发panic |
worker pool(goroutine池)
开启一定数量的goroutine去工作
package main
/*
使用goroutine和channel实现一个计算int64随机数各位数字之和的程序
1、开启一个goroutine循环生成int64类型随机数,发送到jobChan
2、开启24个goroutine循环从jobChan中取出随机数计算各位数字之后,将结果发送到resultChan
3、主goroutine从resultChan中取出结果并打印到终端
*/
import (
"fmt"
"math/rand"
"sync"
"time"
)
type job struct{
num int64
}
type result struct{
job
result int64
}
var jobChan=make(chan *job,100)
var resultChan=make(chan *result,100)
var wg sync.WaitGroup
//生产者
func producer(p chan<- *job){
defer wg.Done()
//循环生成int64
for{
x:=rand.Int63()
newjob := &job{
num:x,
}
p<-newjob
time.Sleep(time.Millisecond*400)
}
}
//消费者
func customer(p <-chan *job,resultChan chan<- *result){
defer wg.Done()
for{
job:=<-p
sum:=int64(0)
n:=job.num
for n>0{
sum+=n%10
n=n/10
}
newResult:=&result{
job:*job,
result: sum,
}
resultChan<-newResult
}
}
func main(){
wg.Add(1)
go producer(jobChan)
//开启24个goroutine
for i:=0;i<24;i++{
wg.Add(1)
go customer(jobChan,resultChan)
}
for result:=range resultChan{
fmt.Println("value:",result.job.num,"-",result.result)
}
wg.Wait()
}
值得注意的是虽然写的协程少读的协程多,不可避免会出现管道中无数据,但是要执行读数据的情况。这时编译器会发现有协程再往管道中写数据,从而没有直接抛出deadlock。
select多路复用
某些场景下我们需要同时从多个通道内接受数据。通道在接受数据时,如果没有数据可以接受将会发生阻塞
for{
//尝试从多个通道接收数据
data,ok:=<-ch1
data,ok:=<ch2
....
}
以上的方式可以实现从多个通道接受值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select
关键字,可以同时响应多个通道的操作。select的使用类似switch语句,他又一些列case分支和一个默认分支。每个case会对应一个通道的接受或发送过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。
语法:
select{
//选哪个case是随机的,不是从上到下
case <-ch1:
....
case data:=<-ch2:
...
case ch3<-data
...
...
default:
...
}
并发安全&锁 sync包
互斥锁
互斥锁:控制共享资源访问的方法,他能保证只有一个goroutine
可以访问共享资源。Go语言中使用sync
包的Mutex
类型来实现互斥锁。
不加锁会输出小于100000的数字,因为两个goroutine同时对x进行+1。加锁能保证同一时间只有一个goroutine访问x。
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add(){
for i:=0;i<50000;i++{
lock.Lock()//加锁
x=x+1
lock.Unlock()//解锁
}
wg.Done()
}
func main(){
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
读写互斥锁
互斥锁是完全互斥的,但是在很多实际场景下是读多写少的,当我们并发去读去一个资源的时候不涉及资源的修改是没必要加锁的,这种场景下,使用读写锁是更好的一种选择。Go语言中使用sync
包的RWMutex
类型来实现读写锁。
读写互斥锁分为两种:读锁和写锁,当一个goroutine获取读锁之后,其他goroutine如果是获取读锁会继续获取读锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他goroutine无论读锁还是写锁都会等待。
package main
import (
"fmt"
"sync"
"time"
)
var(
x int64
rwlock sync.RWMutex
wg sync.WaitGroup
lock sync.Mutex
)
func write(){
//lock.Lock()
rwlock.Lock()
x=x+1
time.Sleep(time.Millisecond*10)
//lock.Unlock()
rwlock.Unlock()
defer wg.Done()
}
func read() {
//lock.Lock()
rwlock.RLock()
time.Sleep(time.Millisecond)
//lock.Unlock()
rwlock.RUnlock()
defer wg.Done()
}
func main(){
start:=time.Now()
for i:=0;i<10;i++{
wg.Add(1)
go write()
}
for i:=0;i<1000;i++{
wg.Add(1)
go read()
}
wg.Wait()
end:=time.Now()
fmt.Println(end.Sub(start))
}
sync.Once
在有些场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync
包中提供了一种针对只执行一次场景的解决方案sync.Once
。他只有一个Do()
方法:func (o *Once)Do(f func()){}
(如果要执行函数f需要传递参数就需要搭配闭包来使用)
注:Do()
方法的参数只能是没有参数列表没有返回值列表的函数
var icons map[string]image.Image
func loadIcons(){
icons=map[string]image.Image{
"left":loadIcon("left.png"),
"up":loadIcon("up.png"),
"right":loadIcon("right.png"),
"down":loadIcon("down.png"),
}
}
//Icon被多个goroutine调用时不是并发安全的
func Icon(name string)image.Image{
if icons==nil{
loadIcons()
}
return icons[name]
}
改写
var icons map[string]image.Image
var loadIconOnce sync.Once
func loadIcons(){
icons=map[string]image.Image{
"left":loadIcon("left.png"),
"up":loadIcon("up.png"),
"right":loadIcon("right.png"),
"down":loadIcon("down.png"),
}
}
//Icon是并发安全的
func Icon(name string)image.Image{
loadIconOnce.Do(loadIcons)
return icons[name]
}
sync.Map
Go语言内置的map不是并发安全的,
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
以上代码会出现fatal error:concurrent map writes
错误,可以在set前后加锁,但是影响效率。错误原因:多个goroutine
修改map时就会触发。
Go语言sync包中提供了一个开箱即用的并发安全map–sync.Map
(不用像内置的map一样使用make函数初始化就能直接使用,还内置了Store、Load、LoadOrStore、Delete、Range等方法)
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)//存值
value, _ := m.Load(key)//取值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
atomic包
提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type Counter interface {
Inc()
Load() int64
}
// 普通版
type CommonCounter struct {
counter int64
}
func (c CommonCounter) Inc() {
c.counter++
}
func (c CommonCounter) Load() int64 {
return c.counter
}
// 互斥锁版
type MutexCounter struct {
counter int64
lock sync.Mutex
}
func (m *MutexCounter) Inc() {
m.lock.Lock()
defer m.lock.Unlock()
m.counter++
}
func (m *MutexCounter) Load() int64 {
m.lock.Lock()
defer m.lock.Unlock()
return m.counter
}
// 原子操作版
type AtomicCounter struct {
counter int64
}
func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}
func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}
func test(c Counter) {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(c.Load(), end.Sub(start))
}
func main() {
c1 := CommonCounter{} // 非并发安全
test(c1)
c2 := MutexCounter{} // 使用互斥锁实现并发安全
test(&c2)
c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
test(&c3)
}
网络编程
基础
服务端
package main
import (
"fmt"
"net"
)
func main(){
//本地端口启动服务
listener,err:=net.Listen("tcp","127.0.0.1:20000")
if err!=nil{
fmt.Println("start server failed, err:",err)
return
}
//等待客户端链接
conn,err:=listener.Accept()
if err!=nil{
fmt.Println("accept failed, err:",err)
return
}
//与客户端通信
var tmp [128]byte
n,err:=conn.Read(tmp[:])
if err!=nil{
fmt.Println("read from conn failed,err:",err)
return
}
fmt.Println(string(tmp[:n]))
}
客户端
package main
import (
"fmt"
"net"
)
//客户端
func main(){
//与server建立连接
conn,err:=net.Dial("tcp","127.0.0.1:20000")
if err!=nil{
fmt.Println("connect failed,err:",err)
return
}
//发送数据
conn.Write([]byte("hello"))
conn.Close()
}
进阶1
服务端
// TCP server端
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")//监听
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
客户端
// 客户端
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
}
黏包
出现原因:tcp数据传输模式是流模式,在保持长时间连接的时候可以进行多次收和发。
粘包可能出现在接收端也可能出现在发送端:1、由Nagle算法造成发送端粘包,在发送的时候,tcp不是立即发送的,而是等待一小段时间看看等待期间是否还有别的要发送的数据,若有则一并发送。2、接收端接受不及时造成接收端粘包,TCP会把接收到的数据存在自己的缓冲区,然后通知应用层取数据。当应用层由于某些原因不能即时把TCP数据取出来,就会造成TCP缓冲去存放几段数据。
服务端
// socket_stick/server/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [1024]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client发来的数据:", recvStr)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
客户端
// socket_stick/client/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
结果
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
解决办法:
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
协议包:
// socket_stick/proto/proto.go
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 将消息编码
func Encode(message string) ([]byte, error) {
// 读取消息的长度,转换成int32类型(占4个字节)
var length = int32(len(message))
var pkg = new(bytes.Buffer)
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)//binary.LittleEndian:小端模式
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
服务端:
// socket_stick/server2/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg, err := proto.Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("decode msg failed, err:", err)
return
}
fmt.Println("收到client发来的数据:", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
客户端:
// socket_stick/client2/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
conn.Write(data)
}
}
net/http包
服务端
Go语言内置的net/http
包提供了Http客户端和服务端的实现
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func f1(w http.ResponseWriter,r *http.Request){
i,err:=ioutil.ReadFile("./xx.txt")//文件写入网页页面
if err!=nil{
fmt.Println("file open failed,err:",err)
}
//str:="hello SYC"
w.Write([]byte(i))
}
func f2(w http.ResponseWriter,r *http.Request){
fmt.Println(r.URL)
fmt.Println(r.Method)
fmt.Println(ioutil.ReadAll(r.Body))
fmt.Println(r.URL.Query())//识别URL中的参数,返回的是map
w.Write([]byte("ok"))
}
func main(){
http.HandleFunc("/posts/Go/hello",f1)//绑定路径对应的处理函数
http.HandleFunc("/xxx",f2)
http.ListenAndServe("127.0.0.1:20000",nil)//最后写
}
客户端
通过后端代码发送http请求
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main(){
resp,err:=http.Get("http://127.0.0.1:20000/xxx")
if err!=nil{
fmt.Println("error:",err)
return
}
//从resp中把服务端返回的数据读出来
b,err:=ioutil.ReadAll(resp.Body)
fmt.Println(string(b))
defer resp.Body.Close()//必须关闭
}
单元测试
Go语言测试依赖go test命令。编写测试代码和编写普通代码过程是类似的。go test命令是一个按照一定约定和组织的测试代码驱动程序。在包目录内,所有以_test.go
为后缀的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数、示例函数
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为BenchMark | 测试函数性能 |
示例函数 | 函数前缀名为Example | 为文档提供示例文档 |
注:可以写在同一个包下,这样就不用导包
测试函数
基本格式:
测试函数名必须Test开头可选的后缀名也必须大写字母开头,参数t用于报告失败和附加的日志信息。
func TestFuncname(t *testing.T){
ret:=xxxxx//程序输出结果
want:-=xxxx//想要得到的结果,可以直接写死
if !reflect.DeepEqual(ret,want){//不能直接比较,需要使用反射
t.Errorf("want :%v but got %v",want,got)
}
}
使用go test命令即可,若成功会返回PASS,失败会返回FAIL
还可以把测试函数的参数和期望结果封装成结构体。设置一个此结构体的数组,用for range来进行测试
性能调优
基准测试
基准测试是以Benchmark为前缀,需要一个*testing.B
类型的参数b,基准测试必须要执行 b.N 次,这样的测试才有对照性。 b.N的值是根据实际情况调整的
func BenchmarkSplit(b *testing.B){
for i:=0;i<b.N;i++{
Split()//测试函数
}
}
基准测试不会默认执行,需要添加 -bench 参数,通过 go test -bench=Split 命令执行基准测试。
性能提升:减少内存申请次数,