首页
友链
统计
留言
更多
直播
壁纸
推荐
我的毛线
院长科技
Search
1
本站官方群:894703859------|诚邀各位大佬的入驻!
418 阅读
2
pxe 自动化安装系统
348 阅读
3
新款螺旋帽子编织#夏凉帽#合股线夏凉帽编织
308 阅读
4
10 个Linux Awk文本处理经典案例
306 阅读
5
软件安装
293 阅读
Linux
yaml
iptables
shell
ansible
ssl
命令
文件管理
用户权限
综合集群架构
三剑客
awk
sed
自动化
pxe
编织
编织视频
监控
prometheus
go
go占位符
vue
vue基础
vue项目
web
Nginx
html
vscode
html标签
html表格
css基础
css定位
css精灵图
code
html5
项目
js
jQuery
面向对象
kubernetes
k8s命令
k8s
k8s搭建
database
clickhouse
常用工具
微软
登录
/
注册
Search
标签搜索
基础
js
Nginx
css
webapi
jQuery
面向对象
command
项目
ansible
用户权限
go
html
文件管理
命令
综合集群架构
k8s
pxe
awk
vscode
JustDoIt
累计撰写
112
篇文章
累计收到
4
条评论
首页
栏目
Linux
yaml
iptables
shell
ansible
ssl
命令
文件管理
用户权限
综合集群架构
三剑客
awk
sed
自动化
pxe
编织
编织视频
监控
prometheus
go
go占位符
vue
vue基础
vue项目
web
Nginx
html
vscode
html标签
html表格
css基础
css定位
css精灵图
code
html5
项目
js
jQuery
面向对象
kubernetes
k8s命令
k8s
k8s搭建
database
clickhouse
常用工具
微软
页面
友链
统计
留言
直播
壁纸
推荐
我的毛线
院长科技
搜索到
4
篇与
的结果
2022-09-19
LeetCode
https://github.com/halfrost/LeetCode-Go
2022年09月19日
23 阅读
0 评论
0 点赞
2022-06-07
最详细的 Go学习笔记总结
第1章 Go介绍package main import "fmt" func main() { fmt.Println("hello word") } 第2章 Go基本语法2.1变量2.1.1. go语言中变量分为局部变量和全局变量 局部变量,是定义在打括号{}内部的变量,打括号内部也是局部变量的作用域 全局变量,是定义在函数和打括号外部{}的变量 2.1.2. 变量声明格式: var 变量名 变量类型 批量声明未初始化的变量var { a int b string c []float32 e struct { x int y string } } 初始化变量var a int = 20 #标准声明格式 var b = 30 #自动推断类型格式 c := 40 #初始化声明格式,首选 2.1.3.变量多重赋值以简单算法交换为例,传统写法如下var a int = 10 var b int = 20 b,a = a,b 2.1.4.匿名变量Go语言中的函数可以返回多个值,而事实上并不是所有返回值都用的上,那么就可以用匿名变量 “_” 替换即可,匿名变量不占用命名空间,也不会分配内存func GetData()(int,int){ return 10,20 } a,_ := GetData() //舍弃第二个返回值 _,b = GetData()//舍弃第一个返回值 2.2 数据类型2.3 打印格式化2.4 数据类型转换Go语言采用数据类型前置加括号的方式进行类型转换,格式如:T(表达式)。T表示要转换的类型a := 10 b := string(a) //将int型转换为string型 c := float32(a) //将int型转换为float型 2.5 常量相对于变量,常量是不变的值。 常量是一个简单的标识符,在程序运行时,不会被修改格式如下: const 标识符 [类型] = 值 const PAI string = "abc" 2.5.1 常量用于枚举 const ( USERNAME = "geinihua" PASSWORD = "geinihua" NETWORK = "tcp" SERVER = "10.247.22.146" PORT = "3306" DATABASE = "demo" ) dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s",USERNAME,PASSWORD,NETWORK,SERVER,PORT,DATABASE) 常量组中如果不指定类型和初始值,则与上一行非空常量值相同const ( a=10 b c ) fmt.PrintLn(a,b,c) //输出结果10 10 10 2.5.2 iota枚举 iota常量自动生成器,每隔一行,自动加1 iota给常量赋值使用 iota遇到下个const,会重置为0 多个常量可以写一个iota,在一个括号里 多重赋值,在同一行,值一样 3. 流程控制 3.1 if 条件判断语句 func max(num1, num2 int) int { /* 声明局部变量 */ var result int if num1 > num2 { result = num1 } else { result = num2 } return result } 3.2 switch 条件选择语句 grade := "" score := 88.5 switch true { case score >=90: grade = "A" case score >=80: grade = "B" case score >=70: grade = "C" default: grade="E" } fmt.Printf("你的登记是: %s\n",grade ) 3.3 for 循环语句 第一种写法: for i:=0;i<=20 ;i++ { fmt.Printf("%d\n",i) } 第二种写法: var i int for i<=20 { fmt.Printf("%d\n",i) } 第三种写法(for ...range): str := "123ABCabc好" for i,value := range str{ fmt.Printf("第 %d 位的ASCII值=%d,字符是%c\n",i,value,value) } 4.Go语言的函数与指针4.1 函数func(参数列表)(返回参数列表){ //函数体 } 4.1.3 函数变量函数变量是把函数作为值保存到变量中.在Golang中,,函数也是一种类型,可以和其他类型一样被保存在变量中type myFunc func(int) bool func main(){ nums:=[]int{10,20,40,16,17,3030,49849,204394,43943,2923,23923,} fmt.Println(filter(nums,isEven)) fmt.Println(filter(nums,isAdd)) } func filter(arr []int, f myFunc) []int { var result []int for _, value := range arr { if f(value) { result = append(result, value) } } return result } func isEven(num int) bool{ if num%2 == 0 { return true }else { return false } } func isAdd(num int) bool{ if num%2 == 0 { return false } return true } 4.1.4 匿名函数匿名函数没有函数名,只有函数体,可以作为一种类型赋值给变量。匿名函数经常被用于实现回调函数、闭包等1.在定义匿名函数的时候就可以直接使用 res1 := func (n1 int, n2 int) int { return n1 + n2 }(10, 30) //括号里的10,30 就相当于参数列表,分别对应n1和n2 fmt.Println("res1=",res1) 2.将匿名函数赋给一个变量 res1 := func (n1 int, n2 int) int { return n1 + n2 } res2 := res1(50,50) fmt.Println("res1=",res2) 3.匿名函数作为回调函数 func vist(list []float64,f func(float64)) { for _,value:=range list{ f(value) } } List := []float64{1,2,5,20,90} vist(List, func(v float64) { sqrt := math.Pow(v,2) fmt.Println(sqrt) }) 4.1.5 闭包//函数f返回了一个函数,返回的这个函数就是一个闭包。这个函数本身中没有定义变量I的,而是引用了它所在的环境(函数f)中的变量i. func f(i int) func() int { return func() int{ i++ return i } } a:=f(0) fmt.Println(a()) //0 fmt.Println(a()) //1 fmt.Println(a()) //2 fmt.Println(a()) //3 4.1.6 可变参数语法格式: func 函数名(参数名...类型)(返回值列表){} 该语法格式定义了一个接受任何数目、任何类型参数的函数。这里特殊语法是三个点"...",在一个变量后面加上三个点,表示从该处开始接受可变参数func Tsum(nums ...int) { fmt.Println(nums) total:=0 for _,val := range nums{ total+=val } fmt.Println( total) } 4.1.7 golang单元测试要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时需要让文件必须以_test结尾单元测试源码文件可以由多个测试用例组成,每个测试用例函数需要以Test为前缀,例如: 格式如下: func TestXXX( t *testing.T ) func sum2(n1 int, args ...int) int { sum := n1 for i := 0; i < len(args); i++ { sum += args[i] } return sum } func TestAvaiableSum(t *testing.T) { res := sum2(1, 23, 34, 56) fmt.Println("res=", res) } 4.2指针指针式存储另一个变量的内存地址的变量。变量是一种使用方便的占位符。一个指针变量可以指向任何一个值的内存地址 在Go语言中使用地址符&来获取变量的地址,一个变量前使用&会返回该变量的内存地址total:=20 fmt.Println("total的内存地址",&total) 4.2.1 声明指针格式:var 指针变量 *指针类型 声明指针,*T是指针变量的类型,它指向T类型的值,*号用于指定变量是一个指针var ip *int //指向整型的指针 var fp *float32 //指向浮点型的指针 指针使用流程1.定义指针变量 2.为指针变量赋值 3.访问指针变量中指向地址的值 获取指针变量指向的变量值:在指针类型的变量前加上号。如atype Student struct { name string age int sex int8 } func TestZhiz(t *testing.T) { s1:=Student{"steven",32,2} s2:=Student{"Sunny",10,1} var a *Student=&s1 //&s1的内存地址 var b *Student=&s2 //&s2的内存地址 fmt.Printf("s1类型为%T,值为%v\n",s1,s1) fmt.Printf("s2类型为%T,值为%v\n",s2,s2) fmt.Printf("a类型为%T,值为%v\n",a,a) fmt.Printf("b类型为%T,值为%v\n",b,b) fmt.Printf("s1的值等于a指针\n") fmt.Printf("s2的值等于b指针\n") fmt.Printf("*a类型为%T,值为%v\n",*a,*a) fmt.Printf("*b类型为%T,值为%v\n",*b,*b) } 空指针 if(ptr != nil) //ptr不是空指针 if(ptr == nil)//ptr是空指针 4.2.2 使用指针1.通过指针修改变量的值//指针修改变量的值 a2:=32 b2:=&a2 fmt.Println("a2的值",a2) //a2的值 32 fmt.Println("b2地址",b2) //b2地址 0xc4200142d8 fmt.Println("b2的值",*b2) //b2的值 32 *b2++ fmt.Println("b2的值",*b2) //b2的值 33 2.使用指针作为函数的参数将基本数据类型的指针作为函数的参数,可以实现对传入数据的修改,这是因为指针作为函数的参数只是赋值了一个指针,指针指向的内存没有发生改变func main(){ orgi:=68 ptr:=&orgi change(ptr) fmt.Println("执行函数后orgi的值",orgi) //执行函数后orgi的值 20 } func change(p *int) { *p=20 } 4.2.3 指针数组//指针数组 //格式:var ptr [3]*string ptrArr:=[COUNT]string{"abc","ABC","123","8888"} i:=0 //定义指针数组 var ptrPoint [COUNT]*string fmt.Printf("%T,%v \n",ptrPoint,ptrPoint) //[4]*string,[<nil> <nil> <nil> <nil>] //将数组中的每个元素地址赋值给指针数组 for i=0;i<COUNT;i++ { ptrPoint[i] = &ptrArr[i] } fmt.Printf("%T,%v \n",ptrPoint,ptrPoint) //[4]*string,[0xc42000e800 0xc42000e810 0xc42000e820 0xc42000e830] //循环取指针数组中的值 for i=0;i<COUNT;i++ { fmt.Printf("a[%d]=%v \n",i, *ptrPoint[i]) //a[0]=abc //a[1]=ABC //a[2]=123 //a[3]=8888 } 4.2.4 指针的指针指向指针的指针变量声明格式如下:var ptr **int//使用两个*号 //指针的指针 var a2 int var ptr2 *int var pptr **int a2=1234 ptr2=&a2 fmt.Println("ptr地址",ptr2) pptr=&ptr fmt.Println("pptr地址",pptr) fmt.Printf("变量a2=%d\n",a2) fmt.Printf("指针变量ptr2=%d\n",*ptr2) fmt.Printf("指向指针的指针量pptr=%d\n",**pptr) //输出结果 /* ptr地址 0xc4200d4140 pptr地址 0xc4200ec000 变量a2=1234 指针变量ptr2=1234 指向指针的指针量pptr=20 */ 4.3 函数的参数传递4.3.1 值传递(传值)值传递是指在调用函数时将实际参数复制一份传递到函数中,不会影响原内容数据 4.3.2 引用传递(传引用)1.引用传递是在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改将影响原内容数据 2.Go中可以借助指针来实现引用传递。函数参数使用指针参数,传参的时候其实是复制一份指针参数,也就是复制了一份变量地址 3.函数的参数如果是指针,当调用函数时,虽然参数是按复制传递的,但此时仅仅只是复制一个指针,也就是一个内存地址,这样不会造成内存浪费、时间开销 函数传int类型的值与引用对比package main import "fmt" func main() { //函数传int类型的值与引用对比 a:=200 fmt.Printf("变量a的内存地址%p,值为:%v\n",&a,a) changeIntVal(a) fmt.Printf("changeIntVal函数调用后变量a的内存地址%p,值为:%v\n",&a,a) changeIntPtr(&a) fmt.Printf("changeIntPtr函数调用后变量a的内存地址%p,值为:%v\n",&a,a) /* 变量a的内存地址0xc420080008,值为:200 changeIntVal函数,传递的参数n的内存地址:0xc420080018,值为:200 changeIntVal函数调用后变量a的内存地址0xc420080008,值为:200 changeIntPtr函数,传递的参数n的内存地址:0xc42008a020,值为:0xc420080008 changeIntPtr函数调用后变量a的内存地址0xc420080008,值为:50 */ } func changeIntVal(n int) { fmt.Printf("changeIntVal函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n) n=90 } func changeIntPtr(n *int) { fmt.Printf("changeIntPtr函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n) *n=50 } 函数传slice类型的值与引用对比import "fmt" func main() { //函数传slice类型的值与引用对比 a:=[]int{1,2,3,4} fmt.Printf("变量a的内存地址%p,值为:%v\n",&a,a) changeSliceVal(a) fmt.Printf("changeSliceVal函数调用后变量a的内存地址%p,值为:%v\n",&a,a) changeSlicePtr(&a) fmt.Printf("changeSlicePtr函数调用后变量a的内存地址%p,值为:%v\n",&a,a) } func changeSliceVal(n []int) { fmt.Printf("changeSliceVal函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n) n[0]=90 } func changeSlicePtr(n *[]int) { fmt.Printf("changeSlicePtr函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n) (*n)[1]=50 } 5.3 map 5.3.1 map概念 Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。 Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的 Map是hash表的一个引用,类型写为:map[key]value,其中的key, value分别对应一种数据类型,如:map[string]string 要求所有的key的数据类型相同,所有value数据类型相同(注:key与value可以有不同的数据类型) 5.3.2 map语法 声明map第一种方法 mapVar := map[key类型]value类型 第二种方法 mapVar := make(map[key类型]value类型) map初始化和遍历 mapVar:=map[string]string{ "a":"t1", "b":"t2", "c":"t3", } //遍历map for key, value := range mapVar { fmt.Printf("key=%v value=%v\n",key,value) } //查看元素在集合中是否存在 if value,ok:=mapVar["aa"];ok { fmt.Println("存在value",value) }else { fmt.Println("不存在value") } 5.3.3 map是引用类型 第6章 Go常用内置包可参考官网 字符串遍历 str:="strings包:遍历带有中文的字符串" for _, value := range []rune(str) { fmt.Printf("%c\n",value) } json序列化和反序列化 map序列化//第1种map声明方式 M2:= map[int]string{ 2:"aa", 3:"bb", } //第2种map声明方式 M:=make(map[int]string) M[1]="aaa" M[2]="bbb" res2,error:=json.Marshal(M2) res,error:=json.Marshal(M) if error!=nil { fmt.Printf("解析错误") } fmt.Printf(string(res2)) fmt.Printf(string(res)) //返回结果 //{"2":"aa","3":"bb"}{"1":"aaa","2":"bbb"} 结构体序列化package main import ( "encoding/json" "fmt" ) type Stu struct { Name string `json:"name"` Age int HIgh bool sex string Class *Class `json:"class"` } type Class struct { Name string Grade int } func main() { //实例化一个数据结构,用于生成json字符串 cla := new(Class) cla.Name = "1班" cla.Grade = 3 stu := Stu{ "张三", 18, true, "男", cla,//指针变量 } //Marshal失败时err!=nil jsonStu, err := json.Marshal(stu) if err != nil { fmt.Println("生成json字符串错误") } //jsonStu是[]byte类型,转化成string类型便于查看 fmt.Println(string(jsonStu)) 从结果中可以看出 只要是可导出成员(变量首字母大写),都可以转成json。因成员变量sex是不可导出的,故无法转成json。 如果变量打上了json标签,如Name旁边的 json:"name" ,那么转化成的json key就用该标签“name”,否则取变量名作为key,如“Age”,“HIgh”。 bool类型也是可以直接转换为json的value值。Channel, complex 以及函数不能被编码json字符串。当然,循环的数据结构也不行,它会导致marshal陷入死循环。 指针变量,编码时自动转换为它所指向的值,如cla变量。 (当然,不传指针,Stu struct的成员Class如果换成Class struct类型,效果也是一模一样的。只不过指针更快,且能节省内存空间。) //反序列化操作Unmarshal() var per_data Stu err2 := json.Unmarshal([]byte(jsonStu),&per_data) if err2 != nil { fmt.Printf("反序列化错误:%v\n", err) } fmt.Printf("personal_json反序列化=%v\n", per_data) fmt.Printf("per_data=%v\n", *per_data.Class) } 第7 章Go面向对象结构体 匿名结构体 和结构体匿名字段 匿名结构体就是没有名字的结构体,无须通过type关键字定义就可以直接使用。创建匿名结构体的时候,同时也要创建结构体对象 //匿名结构体 addr:=struct{ name string age int }{"slaiven",39} fmt.Println(addr) 匿名字段就是在结构体中的字段没有名字,只包含一个没有字段名的类型 如果字段没有名字,那么默认使用类型作为字段名,同一类型只能有一个匿名字段//匿名字段 user:=new(User) user.string="apce" user.int=84 fmt.Printf("名字%v,年龄%v",user.string,user.int) //名字apce,年龄84 结构体嵌套 将一个结构当作另一结构体的字段(属性),这种就是结构体嵌套,可以模拟以下两种关系. 聚合关系:一个类作为另一个类的属性,一定要采用有名字的结构体作为字段 继承关系:一个类作为另一个类的子类。子类与父类的关系。采用匿名字段的形式,匿名字段就该结构体的父类//聚合关系:一个类作为另一个类的属性 type Address struct { province,city string } type Person struct { name string age int address *Address } func TestMoudelStrings(t *testing.T) { //实例化Address结构体 addr:=Address{} addr.province="北京市" addr.city="丰台区" //实例化Person结构体 p:=Person{} p.name="Strven" p.age=28 p.address=&addr fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",p.address.city) //如果修改了Person对象的address数据,那么对Address对象会有影响么?肯定的 p.address.city="大兴区" fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",addr.city) //修改Address对象,是否会影响Persion对象数据?肯定的 addr.city="朝阳区" fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",addr.city) } //继承关系:一个类作为另一个类的子类。子类与父类的关系 type Address struct { province,city string } type Person struct { name string age int Address //匿名字段,Address是Person的父类 } func TestMoudelStrings(t *testing.T) { //实例化Address结构体 addr:=Address{} addr.province="北京" addr.city="丰台区" //实例化Person结构体 p:=Person{"strven",38,addr} fmt.Printf("姓名:%v 年龄:%v 省:%v 市:%v\n",p.name,p.age,p.Address.province,p.Address.city) //姓名:strven 年龄:38 省:北京 市:丰台区 } 方法 Go中同时有函数和方法,方法的本质是函数,但是与函数又不同 1.含义不同,函数是一段具有独立功能的代码,可以被反复多次调用,而方法是一个类的行为功能,只有该类的对象才能调用 2.方法有接受者而函数没有,Go语言的方法是一种作用域特定类型变量的函数,这种类型变量叫作接受者(receiver),接受者的概念类似于传统面向对象中的this或self关键字 3.方法可以重名(接受者不同),而函数不能重名,type Per struct { name string age int } func ( p Per ) getData() { fmt.Printf("名字:%v 年龄:%v",p.name,p.age) //名字:aaa 年龄:39 } func TestMethod(t *testing.T) { p1:=Per{"aaa",39} p1.getData() } 方法继承 方法是可以继承的,如果匿名字段实现了一个方法,那么包含这个匿名字段的struct也能调用该匿名字段中的方法type Human struct { name, phone string age int } type Stu struct { Human school string } type Employee struct { Human company string } func TestMethod(t *testing.T) { s1:=Stu{Human{"dav","1850103930",7}," 洛阳一中"} s1.SayHi() } func (h *Human) SayHi() { fmt.Printf("我是%s,%d岁,电话%s\n",h.name,h.age,h.phone) } 方法重写 type Human struct { name, phone string age int } type Stu struct { Human school string } type Employee struct { Human company string } func TestMethod(t *testing.T) { s1:=Stu{Human{"dav","1850103930",7}, " 洛阳一中"} s2:=Employee{Human{"dav","1850*****",17},"航天飞机"} s1.SayHi() s2.SayHi() } func (h *Human) SayHi() { fmt.Printf("我是%s,%d岁,电话%s\n",h.name,h.age,h.phone) } func (h *Stu) SayHi() { fmt.Printf("我是%s,%d岁,电话%s,学校%s\n",h.name,h.age,h.phone,h.school) } func (h *Employee) SayHi() { fmt.Printf("我是%s,%d岁,电话%s,工作%s\n",h.name,h.age,h.phone,h.company) } 第9章 Go文件I/O操作9.1文件信息 FileInfo接口 func main() { file:="./layout.html" printMessat(file) } func printMessat(filePath string) { fileinfo,error:=os.Stat(filePath) if error !=nil { fmt.Println("文件打开错误",error.Error()) } fmt.Printf("文件名:%s\n",fileinfo.Name()) fmt.Printf("文件权限:%s\n",fileinfo.Mode()) fmt.Printf("是否为目录:%s\n",fileinfo.IsDir()) fmt.Printf("文件最后修改权限:%s\n",fileinfo.ModTime()) fmt.Printf("文件大小:%s\n",fileinfo.Size()) } 文件路径 file1:="/Users/u51/Documents/go_learn/first.go" file2:="./layout.html" fmt.Printf("是否是绝对路径%v\n",filepath.IsAbs(file1)) fmt.Printf("是否是绝对路径%v\n",filepath.IsAbs(file2)) fmt.Printf("获取文件绝对路径%v\n",filepath.Abs(file2)) fmt.Printf("获取文件绝对路径%v\n",filepath.Abs(file2)) 9.2 文件常规操作 创建目录 os.MKdir() 创建一级目录 os.MKdirAll() 创建多级目录 创建文件 os.create() 创建文件 删除文件 os.Remove() 删除文件或空目录 os.RemoveAll() 移除所有所有路径及包含的子节点,file,error:=os.Create("test1.csv") if error!=nil{ fmt.Printf("文件创建失败") } file.Write([]byte("aaa\n")) file.WriteString("bbbb") file.WriteString("文件") 打开和关闭文件 os.Open从文件开始读取数据,返回值n是实际读取的字节数,如果读到文件末尾n为0或err为io.EOFfile,error:=os.Open("./test1.csv") if error!=nil{ fmt.Printf("打开错误") }else { bs:=make([]byte,1024,1024) for{ n,err:=file.Read(bs) if n==0||err==io.EOF{ fmt.Printf("读取文件结束-----") break } fmt.Println(string(bs[:])) } } defer file.Close() 复制文件 func main() { srcFile:="./test.csv" destFile:="./test1.csv" total,err:=copyFiles(srcFile,destFile) if err!=nil{ fmt.Printf(err.Error()) }else { fmt.Println("复制ok",total) } } func copyFiles(srcfile,destfile string)(int64,error) { file1,err:=os.Open(srcfile) if err!=nil{ return 0, err } file2,err:=os.OpenFile(destfile,os.O_RDWR|os.O_CREATE,os.ModePerm) if err != nil{ return 0, err } defer file1.Close() defer file2.Close() return io.Copy(file2,file1) } 9.3 ioutil包 ioutil包核心函数 方法 作用 ReadFile() 读取文件中所有数据,返回读取的字节数组 WriteFile() 向指定文件写入数据,如果文件不存在,则创建文件,写入文件之前清空文件 ReadDir() 读取一个目录下的所有子文件及目录名称 TempDir() 在当前目录下,创建一个以指定字符串为名称前缀的临时文件夹,并返回文件夹路径 TempFile() 在当前目录下,创建一个以指定字符串为名称的前缀的文件,并以读写模式打开,返回os.File指针 data,error:=ioutil.ReadFile("./test1.csv") if error!=nil{ fmt.Printf("打开错误") }else { fmt.Println(string(data)) } bs:=[]byte("hello中的空间打开") error:=ioutil.WriteFile("./test1.csv",bs,077) if error!=nil{ fmt.Printf("写入文件异常") }else { fmt.Printf("写入文件成功") } 9.4 bufio包bufio实现了带缓冲的I/O操作,达到高效读写 bufio.Reader结构体 bufio.Reader 常用方法 方法 作用 func NewReader(rd io.Reader) *Reader 创建一个具有默认大小缓冲区、从r读取的*Reader func NewReaderSize(rd io.Reader, size int) *Reader 创建一个具有size大小缓冲区,从r读取的*Reader func (b *Reader) Discard(n int) (discarded int, err error) 丢弃n个byte数据 func (b *Reader) Read(p []byte) (n int, err error) 读取n个byte数据 func (b *Reader) Buffered() int 返回缓冲区中现有的可读取的字节数 func (b *Reader) Peek(n int) ([]byte, error) 获取当前缓冲区内接下来的n个byte数据,但不是移动指针 func (b *Reader) ReadByte() (byte, error) 读取1个字节 func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) 读取一行数据,\n分隔 func (b *Reader) ReadString(delim byte) (string, error) 读取1个字符串 func (b *Reader) ReadRune() (r rune, size int, err error) 读取1个utf-8字符 func (b *Reader) Reset(r io.Reader) 清空缓冲区 bufio.Writer结构体 第10章 并行编程并发与并行 并发:同一时间段内执行多个任务 并行:同一时刻执行多个任务 Go语言的并发通过goroutine实现,goroutine类似于线程,属于用户态线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。 Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go 语言并发模式的重要实现基础。 goroutine goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴 Go语言中使用goroutine非常简单,只需要在被调用的函数前面加上go 关键字,这样就可以为一个函数创建一个goroutine(也就是创建了一个线程) 一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。启动单个goroutinepackage main func t1Goroutine() { println("第一个goroutine函数") } func main() { go t1Goroutine() println("main over....") } //输出结果,为什么没有打印函数t1Goroutine中的内容呢???? main over.... 原因: 在程序启动时,Go程序就会为main()函数创建一个默认的goroutine.当main()函数返回的时候该goroutine就结束了,所有在main()中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了。执行上面代码会发现,先打印main over....然后再打印goroutine的内容 第一个goroutine函数,首先为什么会先打印main over....是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的启动多个goroutinepackage main import ( "fmt" "sync" ) //wait group 用来等待一组goroutines的结束,在主Goroutine里声明,并且设置要等待的goroutine的个数,每个goroutine执行完成之后调用 Done,最后在主Goroutines 里Wait即可 var wg sync.WaitGroup //声明一个全局变量 func t2Goroutine(i int ) { defer wg.Done() fmt.Println("Go ", i) } func main() { for i:=0;i<10;i++{ wg.Add(1) go t2Goroutine(i) } wg.Wait() } //输出结果 Go 5 Go 1 Go 0 Go 7 Go 8 Go 2 Go 6 Go 9 Go 4 Go 3 多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。goroutine与线程可增长的栈OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的。goroutine调度GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。 G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。 P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。 M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的; 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调度方面的性能GOMAXPROCSGo运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码直接在main中声明runtime.GOMAXPROCS(1)channel通道如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。声明channel类型var by1 []int //声明一个int类型的切片 var ch1 chan int //声明一个存放int类型的channel 声明的通道后需要使用make函数初始化之后才能使用。 // 初始化channel ch2:=make(chan int) // 初始化带缓冲区的chan ch3:=make(chan string) channel操作通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都使用<-符号。 现在我们先使用以下语句定义一个通道: ch := make(chan int) 将一个值发送到通道中ch<- 10//把数据10发送到ch中 从一个通道中接收值 data:= <-ch// 从ch中接收的值赋值给变量data <-ch //从ch中接收值,忽略结果 我们通过调用内置的close函数来关闭通道。close(ch) 关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。关闭后的通道有以下特点: 对一个关闭的通道再发送值就会导致panic。 对一个关闭的通道进行接收会一直获取值直到通道为空。 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。 关闭一个已经关闭的通道会导致panic。 无缓冲的通道无缓冲通道又称为阻塞的通道func main() { // 初始化channel ch1:=make(chan int) ch1 <- 10 fmt.Printf("发送成功") } //输出结果 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() D:/work/workspace/go_space/goruntine.go:8 +0x5f 为什么会出现deadlock错误呢?因为我们使用ch1 := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?一种方法是启用一个goroutine去接收值,例如:package main import "fmt" func recvData(c chan int) { x:=<-c fmt.Println("接收到的数据",x) } func main() { // 初始化channel ch1:=make(chan int) go recvData(ch1) ch1 <- 10 fmt.Printf("发送成功") } //输出结果 接收到的数据 10 发送成功 无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道有缓冲的通道解决上面问题的方法还有一种,使用带有缓冲区的通道func main() { // 初始化channel ch1:=make(chan int ,2) ch1 <- 10 fmt.Printf("发送成功") } 从通道循环取值当向通道中发送完数据时,我们可以通过close函数来关闭通道。package main import ( "fmt" ) func main() { ch1:=make(chan int) ch2:=make(chan int) // 循环生成100个数,存到ch1中 go func() { for i:=0;i<10;i++{ ch1<-i } close(ch1) }() // 从ch1中接收值,*2后存到ch2 go func() { for{ data,ok:=<-ch1 //通道关闭后再取值ok=false if !ok{ break } ch2<-data*data } close(ch2) }() //在主goroutine中range ch2 for i:=range ch2{// 通道关闭后会退出for range循环 fmt.Println(i) } } //输出结果 0 1 4 9 16 25 36 49 64 81 从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range单向通道 chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作; <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。 第11章 反射反射可以在程序编译期将变量的信息如字段名称、类型、结构体信息等整合到可执行文件中,这样就可以在程序运行期获取类型的反射信息Go程序在运行期使用reflect包访问程序的反射信息。在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。reflect.TypeOf() 获取任意值的 类型对象 func reflectType(x interface{}) { v := reflect.TypeOf(x) fmt.Printf("type:%v\n", v) } func main() { var a float32 = 3.14 reflectType(a) // type:float32 var b int64 = 100 reflectType(b) // type:int64 } type name和type kind 在反射中关于类型还划分为两种:类型(Type)和种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。type person struct { Name string `json:"name"` Age int `json:"age"` } func reflectType(x interface{}) { v := reflect.TypeOf(x) fmt.Printf("type:%v kind:%v\n", v.Name(), v.Kind()) } func main() { var a float32 = 3.14 reflectType(a) // type:float32 var b int64 = 100 reflectType(b) // type:int64 p:=person{ "struc", 20, } reflectType(p) //type:person kind:struct m:=make(map[int]string) reflectType(m) //type: kind:map var s []string reflectType(s) //type: kind:slice } reflect.ValueOf()reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。 func reValueOf(x interface{}) { v:=reflect.ValueOf(x) k:=v.Kind() switch k { case reflect.Int64: // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换 fmt.Printf("type is int64, value is %d\n", int64(v.Int())) case reflect.Int32: fmt.Printf("type is int32, value is %d\n", int32(v.Int())) } } func main() { var b int64 = 100 reValueOf(b) // type is int64, value is 100 } 通过反射设置变量值 想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值func relectSetVal(x interface{}) { v:=reflect.ValueOf(x) //反射中使用Elem()方法获取指针对应的值 if v.Elem().Kind()==reflect.Int64{ v.Elem().SetInt(200) } } func main() { var b int64 = 100 relectSetVal(&b) fmt.Println(b) //200 } 结构体反射 任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。 type person struct { Name string `json:"name"` Age int `json:"age"` } func main() { p:=person{"test",20} r:=reflect.TypeOf(p) fmt.Println(r.NumField(),r.Name(),r.Kind()) //2 person struct //判断kind是struct if r.Kind()==reflect.Struct{ // 通过for循环遍历结构体的所有字段信息 for i:=0;i<r.NumField();i++{ field := r.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 name_field,ok:=r.FieldByName("Name");ok{ fmt.Printf("name:%s index:%d type:%v json tag:%v\n", name_field.Name, name_field.Index, name_field.Type, name_field.Tag.Get("json")) } } 第12章 worker pool(goroutine池)第13章 网络编程get请求func main() { resp, err := http.Get("http://www.baidu.com/") if err != nil { fmt.Printf("get failed, err:%v\n", err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("read from resp.Body failed, err:%v\n", err) return } fmt.Println(string(body)) } 带参数的get请求关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。func httpGet2(requestUrl string) (err error) { Url, err := url.Parse(requestUrl) if err != nil { fmt.Printf("requestUrl parse failed, err:[%s]", err.Error()) return } params := url.Values{} params.Set("username","googlesearch") params.Set("passwd","golang") Url.RawQuery = params.Encode() requestUrl = Url.String() fmt.Printf("requestUrl:[%s]\n", requestUrl) resp, err := http.Get(requestUrl) if err != nil { fmt.Printf("get request failed, err:[%s]", err.Error()) return } defer resp.Body.Close() bodyContent, err := ioutil.ReadAll(resp.Body) fmt.Printf("resp status code:[%d]\n", resp.StatusCode) fmt.Printf("resp body data:[%s]\n", string(bodyContent)) return } 带参数post请求func httpPost(requestUrl string) (err error) { data := url.Values{} data.Add("username", "seemmo") data.Add("passwd", "da123qwe") resp, err := http.PostForm(requestUrl, data) if err != nil { fmt.Printf("get request failed, err:[%s]", err.Error()) return } defer resp.Body.Close() bodyContent, err := ioutil.ReadAll(resp.Body) fmt.Printf("resp status code:[%d]\n", resp.StatusCode) fmt.Printf("resp body data:[%s]\n", string(bodyContent)) return } TCP连接过程TCP服务端程序的处理流程: 1.监听端口 2.接收客户端请求建立连接 3.创建goroutine处理连接 TCP客户端程序的处理流程: 1.建立与服务端的链接 2.进行数据收发 3.关闭链接 服务端代码 package main import ( "bufio" "fmt" "net" ) //实现TCP通信步骤: //1.监听端口 //2.接收客户端请求建立连接 //3.创建goroutine处理连接 func process(conn net.Conn) { defer conn.Close() //延迟关闭 reader:=bufio.NewReader(conn) var b [128]byte for { n,err:=reader.Read(b[:]) if err!=nil{ fmt.Println("read from client failed, err:", err) break } recvStr := string(b[:n]) fmt.Println("收到client端发来的数据:", recvStr) conn.Write([]byte(recvStr)) // 发送数据 } } func main() { listen,err:=net.Listen("tcp","127.0.0.1:9090") 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) } } 客户端代码package main import ( "bufio" "fmt" "net" "os" "strings" ) func main() { conn,err:=net.Dial("tcp","127.0.0.1:9090") if err!=nil{ fmt.Println("err:",err) return } defer conn.Close() fmt.Println("请输入数据") inputReder:=bufio.NewReader(os.Stdin) for{ input,_:=inputReder.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])) } } 第14章 数据库操作下载依赖go get -u github.com/go-sql-driver/mysql 创建dbconf包package dbconf import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) // 定义一个全局对象db var db *sql.DB func InitDb() (err error) { dsn := "epai:epai@tcp(10.10.10.48:3306)/bind_dns?charset=utf8mb4&parseTime=True" // 不会校验账号密码是否正确 // 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db db,err = sql.Open("mysql", dsn) if err != nil { return } // 尝试与数据库建立连接(校验dsn是否正确) err = db.Ping() if err != nil { return } return } type binddns struct { zone string data string } func QueryRowDemo() { var dns binddns sqlStr := "SELECT zone, data from dns_records" rows,err:=db.Query(sqlStr) defer rows.Close() if err !=nil { fmt.Printf("query failed, err:%v\n", err) } for rows.Next() { err:=rows.Scan(&dns.zone,&dns.data) if err != nil{ fmt.Printf("scan failed, err:%v\n", err) return } fmt.Printf("域名:%s 记录值:%s\n", dns.zone,dns.data) } } func InserDemo() { sqlstr := "INSERT INTO `dns_records` VALUES (9, 'abc.com', '*', 'A', '10.10.10.83', 60, NULL, 'any', 255, 28800, 14400, 86400, 86400, 2015050917, 'ddns.net', 'ns.ddns.net.');" res, error := db.Exec(sqlstr) if error != nil { fmt.Printf("insert failed, err:%v\n", error) return } theID, err := res.LastInsertId() // 新插入数据的id if err != nil { fmt.Printf("get lastinsert ID failed, err:%v\n", err) return } fmt.Printf("insert success, the id is %d.\n", theID) } func UpdateDemo() { sqlstr:="update dns_records set data=? where id = ?" res,err:=db.Exec(sqlstr,"10.10.10.98",9) if err !=nil{ fmt.Printf("update failed, err:%v\n", err) return } res_row,err:=res.RowsAffected() if err !=nil{ fmt.Printf("get RowsAffected failed, err:%v\n", err) return } fmt.Printf("update success, affected rows:%d\n", res_row) } 在main.go引入dbconf包package main import ( "fmt" "test_go_mod2/dbconf" ) func main() { err:=dbconf.InitDb() if err != nil { fmt.Printf("init db failed,err:%v\n", err) return }else { fmt.Println("连接mysql成功") } dbconf.QueryRowDemo() dbconf.InserDemo() dbconf.UpdateDemo() } 预处理为什么预处理?优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。 避免SQL注入问题。 Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。//预查询 func PreareDemo() { sqlStr := "SELECT zone, data from dns_records where id < ?" stmt,err:=db.Prepare(sqlStr) if err !=nil { fmt.Printf("preare query failed, err:%v\n", err) } rows,err:=stmt.Query(8) defer rows.Close() if err !=nil { fmt.Printf("query failed, err:%v\n", err) } for rows.Next() { err:=rows.Scan(&dns.zone,&dns.data) if err != nil{ fmt.Printf("scan failed, err:%v\n", err) return } fmt.Printf("域名:%s 记录值:%s\n", dns.zone,dns.data) } } 15章 gorm操作作者:yum玩坏了链接:https://juejin.cn/post/6903353632687652872来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2022年06月07日
73 阅读
0 评论
0 点赞
2022-03-15
Go语言fmt占位符
占位符 说明 %T 查看类型 %v 输入相应值的默认格式 %b 查看二进制 %o 查看八进制 %d 查看十进制 %x 查看十六进制,字母形式为小写 a-f %X 查看十六进制,字母形式为小写 A-F %U 查看Unicode格式 %p 查看指针 package main import "fmt" func main() { var name = "Vitas" var age = 18 var sex = "male" fmt.Printf("name:%v\n",name) fmt.Printf("age:%v\n",age) fmt.Printf("sex:%v\n",sex) fmt.Printf("name的类型:%T\n",name) fmt.Printf("age的类型:%T\n",age) fmt.Printf("sex的类型:%T\n",sex) fmt.Printf("age的二进制:%b\n",age) fmt.Printf("age的八进制:%o\n",age) fmt.Printf("age的十进制:%d\n",age) fmt.Printf("age的十六进制:%x\n",age) fmt.Printf("name的指针:%p\n",&name) } 打印结果如下 name:Vitas age:18 sex:male name的类型:string age的类型:int sex的类型:string age的二进制:10010 age的八进制:22 age的十进制:18 age的十六进制:12 name的指针:0xc00008e1e0
2022年03月15日
153 阅读
0 评论
1 点赞
2022-03-02
反射reflect
一、引入先看官方Doc中Rob Pike给出的关于反射的定义:Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion. (在计算机领域,反射是一种让程序——主要是通过类型——理解其自身结构的一种能力。它是元编程的组成之一,同时它也是一大引人困惑的难题。) 维基百科中的定义:在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。 不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的:Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。为什么要用反射需要反射的 2 个常见场景: 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。 但是对于反射,还是有几点不太建议使用反射的理由: 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。 Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。 二、相关基础反射是如何实现的?我们以前学习过 interface,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。在进行更加详细的了解之前,我们需要重新温习一下Go语言相关的一些特性,所谓温故知新,从这些特性中了解其反射机制是如何使用的。 特点 说明 go语言是静态类型语言。 编译时类型已经确定,比如对已基本数据类型的再定义后的类型,反射时候需要确认返回的是何种类型。 空接口interface{} go的反射机制是要通过接口来进行的,而类似于Java的Object的空接口可以和任何类型进行交互,因此对基本数据类型等的反射也直接利用了这一特点 Go语言的类型: 变量包括(type, value)两部分 理解这一点就知道为什么nil != nil了 type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型 类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer。 Go是静态类型语言。每个变量都拥有一个静态类型,这意味着每个变量的类型在编译时都是确定的:int,float32, *AutoType, []byte, chan []int 诸如此类。在反射的概念中, 编译时就知道变量类型的是静态类型;运行时才知道一个变量类型的叫做动态类型。 静态类型静态类型就是变量声明时的赋予的类型。比如: type MyInt int // int 就是静态类型 type A struct{ Name string // string就是静态 } var i *int // *int就是静态类型 动态类型动态类型:运行时给这个变量赋值时,这个值的类型(如果值为nil的时候没有动态类型)。一个变量的动态类型在运行时可能改变,这主要依赖于它的赋值(前提是这个变量是接口类型)。 var A interface{} // 静态类型interface{} A = 10 // 静态类型为interface{} 动态为int A = "String" // 静态类型为interface{} 动态为string var M *int A = M // A的值可以改变 Go语言的反射就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:(value, type) value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) var r io.Reader r = tty 接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:var w io.Writer w = r.(io.Writer) 接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。所以我们要理解两个基本概念 Type 和 Value,它们也是 Go语言包中 reflect 空间里最重要的两个类型。三、Type和Value我们一般用到的包是reflect包。既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释// ValueOf returns a new Value initialized to the concrete value // stored in the interface i. ValueOf(nil) returns the zero func ValueOf(i interface{}) Value {...} 翻译一下:ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0 // TypeOf returns the reflection Type that represents the dynamic type of i. // If i is a nil interface value, TypeOf returns nil. func TypeOf(i interface{}) Type {...} 翻译一下:TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value。首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数。t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值 示例代码:package main import ( "fmt" "reflect" ) func main() { //反射操作:通过反射,可以获取一个接口类型变量的 类型和数值 var x float64 =3.4 fmt.Println("type:",reflect.TypeOf(x)) //type: float64 fmt.Println("value:",reflect.ValueOf(x)) //value: 3.4 fmt.Println("-------------------") //根据反射的值,来获取对应的类型和数值 v := reflect.ValueOf(x) fmt.Println("kind is float64: ",v.Kind() == reflect.Float64) fmt.Println("type : ",v.Type()) fmt.Println("value : ",v.Float()) } 运行结果:type: float64 value: 3.4 ------------------- kind is float64: true type : float64 value : 3.4 说明 reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型 reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 "Allen.Wu" 25} 这样的结构体struct的值 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种 Type 和 Value 都包含了大量的方法,其中第一个有用的方法应该是 Kind,这个方法返回该类型的具体信息:Uint、Float64 等。Value 类型还包含了一系列类型方法,比如 Int(),用于返回对应的值。以下是Kind的种类:// A Kind represents the specific kind of type that a Type represents. // The zero Kind is not a valid kind. type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer ) 四、反射的规则其实反射的操作步骤非常的简单,就是通过实体对象获取反射对象(Value、Type),然后操作相应的方法即可。下图描述了实例、Value、Type 三者之间的转换关系:反射 API 的分类总结如下:1) 从实例到 Value通过实例获取 Value 对象,直接使用 reflect.ValueOf() 函数。例如:func ValueOf(i interface {}) Value 2) 从实例到 Type通过实例获取反射对象的 Type,直接使用 reflect.TypeOf() 函数。例如:func TypeOf(i interface{}) Type 3) 从 Type 到 ValueType 里面只有类型信息,所以直接从一个 Type 接口变量里面是无法获得实例的 Value 的,但可以通过该 Type 构建一个新实例的 Value。reflect 包提供了两种方法,示例如下://New 返回的是一个 Value,该 Value 的 type 为 PtrTo(typ),即 Value 的 Type 是指定 typ 的指针类型 func New(typ Type) Value //Zero 返回的是一个 typ 类型的零佳,注意返回的 Value 不能寻址,位不可改变 func Zero(typ Type) Value 如果知道一个类型值的底层存放地址,则还有一个函数是可以依据 type 和该地址值恢复出 Value 的。例如:func NewAt(typ Type, p unsafe.Pointer) Value 4) 从 Value 到 Type从反射对象 Value 到 Type 可以直接调用 Value 的方法,因为 Value 内部存放着到 Type 类型的指针。例如:func (v Value) Type() Type 5) 从 Value 到实例Value 本身就包含类型和值信息,reflect 提供了丰富的方法来实现从 Value 到实例的转换。例如://该方法最通用,用来将 Value 转换为空接口,该空接口内部存放具体类型实例 //可以使用接口类型查询去还原为具体的类型 func (v Value) Interface() (i interface{}) //Value 自身也提供丰富的方法,直接将 Value 转换为简单类型实例,如果类型不匹配,则直接引起 panic func (v Value) Bool () bool func (v Value) Float() float64 func (v Value) Int() int64 func (v Value) Uint() uint64 6) 从 Value 的指针到值从一个指针类型的 Value 获得值类型 Value 有两种方法,示例如下。//如果 v 类型是接口,则 Elem() 返回接口绑定的实例的 Value,如采 v 类型是指针,则返回指针值的 Value,否则引起 panic func (v Value) Elem() Value //如果 v 是指针,则返回指针值的 Value,否则返回 v 自身,该函数不会引起 panic func Indirect(v Value) Value 7) Type 指针和值的相互转换指针类型 Type 到值类型 Type。例如://t 必须是 Array、Chan、Map、Ptr、Slice,否则会引起 panic //Elem 返回的是其内部元素的 Type t.Elem() Type 值类型 Type 到指针类型 Type。例如://PtrTo 返回的是指向 t 的指针型 Type func PtrTo(t Type) Type 8) Value 值的可修改性Value 值的修改涉及如下两个方法://通过 CanSet 判断是否能修改 func (v Value ) CanSet() bool //通过 Set 进行修改 func (v Value ) Set(x Value) Value 值在什么情况下可以修改?我们知道实例对象传递给接口的是一个完全的值拷贝,如果调用反射的方法 reflect.ValueOf() 传进去的是一个值类型变量, 则获得的 Value 实际上是原对象的一个副本,这个 Value 是无论如何也不能被修改的。根据 Go 官方关于反射的博客,反射有三大定律: Reflection goes from interface value to reflection object. Reflection goes from reflection object to interface value. To modify a reflection object, the value must be settable.第一条是最基本的:反射可以从接口值得到反射对象。 反射是一种检测存储在 interface中的类型和值机制。这可以通过 TypeOf函数和 ValueOf函数得到。第二条实际上和第一条是相反的机制,反射可以从反射对象获得接口值。 它将 ValueOf的返回值通过 Interface()函数反向转变成 interface变量。前两条就是说 接口型变量和 反射类型对象可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Type和 reflect.Value。第三条不太好懂:如果需要操作一个反射变量,则其值必须可以修改。 反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。 五、反射的使用5.1 从relfect.Value中获取接口interface的信息当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。已知原有类型已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:realValue := value.Interface().(已知的类型) 示例代码:package main import ( "fmt" "reflect" ) func main() { var num float64 = 1.2345 pointer := reflect.ValueOf(&num) value := reflect.ValueOf(num) // 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic // Golang 对类型要求非常严格,类型一定要完全符合 // 如下两个,一个是*float64,一个是float64,如果弄混,则会panic convertPointer := pointer.Interface().(*float64) convertValue := value.Interface().(float64) fmt.Println(convertPointer) fmt.Println(convertValue) } 运行结果:0xc000098000 1.2345 说明 转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格! 转换的时候,要区分是指针还是指 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量” 未知原有类型很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:package main import ( "fmt" "reflect" ) type Person struct { Name string Age int Sex string } func (p Person)Say(msg string) { fmt.Println("hello,",msg) } func (p Person)PrintInfo() { fmt.Printf("姓名:%s,年龄:%d,性别:%s\n",p.Name,p.Age,p.Sex) } func main() { p1 := Person{"王二狗",30,"男"} DoFiledAndMethod(p1) } // 通过接口来获取任意参数 func DoFiledAndMethod(input interface{}) { getType := reflect.TypeOf(input) //先获取input的类型 fmt.Println("get Type is :", getType.Name()) // Person fmt.Println("get Kind is : ", getType.Kind()) // struct getValue := reflect.ValueOf(input) fmt.Println("get all Fields is:", getValue) //{王二狗 30 男} // 获取方法字段 // 1. 先获取interface的reflect.Type,然后通过NumField进行遍历 // 2. 再通过reflect.Type的Field获取其Field // 3. 最后通过Field的Interface()得到对应的value for i := 0; i < getType.NumField(); i++ { field := getType.Field(i) value := getValue.Field(i).Interface() //获取第i个值 fmt.Printf("字段名称:%s, 字段类型:%s, 字段数值:%v \n", field.Name, field.Type, value) } // 通过反射,操作方法 // 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历 // 2. 再公国reflect.Type的Method获取其Method for i := 0; i < getType.NumMethod(); i++ { method := getType.Method(i) fmt.Printf("方法名称:%s, 方法类型:%v \n", method.Name, method.Type) } } 运行结果:get Type is : Person get Kind is : struct get all Fields is: {王二狗 30 男} 字段名称:Name, 字段类型:string, 字段数值:王二狗 字段名称:Age, 字段类型:int, 字段数值:30 字段名称:Sex, 字段类型:string, 字段数值:男 方法名称:PrintInfo, 方法类型:func(main.Person) 方法名称:Say, 方法类型:func(main.Person, string) 说明通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为: 先获取interface的reflect.Type,然后通过NumField进行遍历 再通过reflect.Type的Field获取其Field 最后通过Field的Interface()得到对应的value 通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为: 先获取interface的reflect.Type,然后通过NumMethod进行遍历 再分别通过reflect.Type的Method获取对应的真实的方法(函数) 最后对结果取其Name和Type得知具体的方法名 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量” struct 或者 struct 的嵌套都是一样的判断处理方式 如果是struct的话,可以使用Elem()tag := t.Elem().Field(0).Tag //获取定义在struct里面的Tag属性 name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值 5.2 通过reflect.Value设置实际变量的值reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。这里需要一个方法:解释起来就是:Elem返回接口v包含的值或指针v指向的值。如果v的类型不是interface或ptr,它会恐慌。如果v为零,则返回零值。package main import ( "fmt" "reflect" ) func main() { var num float64 = 1.2345 fmt.Println("old value of pointer:", num) // 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值 pointer := reflect.ValueOf(&num) newValue := pointer.Elem() fmt.Println("type of pointer:", newValue.Type()) fmt.Println("settability of pointer:", newValue.CanSet()) // 重新赋值 newValue.SetFloat(77) fmt.Println("new value of pointer:", num) //////////////////// // 如果reflect.ValueOf的参数不是指针,会如何? //pointer = reflect.ValueOf(num) //newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value” } 运行结果:old value of pointer: 1.2345 type of pointer: float64 settability of pointer: true new value of pointer: 77 说明 需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针。 如果传入的参数不是指针,而是变量,那么 通过Elem获取原始值对应的对象则直接panic 通过CanSet方法查询是否可以设置返回false newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。 reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的 也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】 struct 或者 struct 的嵌套都是一样的判断处理方式 5.3 通过reflect.Value来进行方法的调用这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、以及如何重新设置新值。但是在项目应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定。Call()方法:通过反射,调用方法。先获取结构体对象,然后示例代码:package main import ( "fmt" "reflect" ) type Person struct { Name string Age int Sex string } func (p Person)Say(msg string) { fmt.Println("hello,",msg) } func (p Person)PrintInfo() { fmt.Printf("姓名:%s,年龄:%d,性别:%s\n",p.Name,p.Age,p.Sex) } func (p Person) Test(i,j int,s string){ fmt.Println(i,j,s) } // 如何通过反射来进行方法的调用? // 本来可以用结构体对象.方法名称()直接调用的, // 但是如果要通过反射, // 那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call func main() { p2 := Person{"Ruby",30,"男"} // 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value, // 得到“反射类型对象”后才能做下一步处理 getValue := reflect.ValueOf(p2) // 2.一定要指定参数为正确的方法名 // 先看看没有参数的调用方法 methodValue1 := getValue.MethodByName("PrintInfo") fmt.Printf("Kind : %s, Type : %s\n",methodValue1.Kind(),methodValue1.Type()) methodValue1.Call(nil) //没有参数,直接写nil args1 := make([]reflect.Value, 0) //或者创建一个空的切片也可以 methodValue1.Call(args1) // 有参数的方法调用 methodValue2 := getValue.MethodByName("Say") fmt.Printf("Kind : %s, Type : %s\n",methodValue2.Kind(),methodValue2.Type()) args2 := []reflect.Value{reflect.ValueOf("反射机制")} methodValue2.Call(args2) methodValue3 := getValue.MethodByName("Test") fmt.Printf("Kind : %s, Type : %s\n",methodValue3.Kind(),methodValue3.Type()) args3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200),reflect.ValueOf("Hello")} methodValue3.Call(args3) } 运行结果:Kind : func, Type : func() 姓名:Ruby,年龄:30,性别:男 姓名:Ruby,年龄:30,性别:男 Kind : func, Type : func(string) hello, 反射机制 Kind : func, Type : func(int, int, string) 100 200 Hello 通过反射,调用函数。首先我们要先确认一点,函数像普通的变量一样,之前的章节中我们在讲到函数的本质的时候,是可以把函数作为一种变量类型的,而且是引用类型。如果说Fun()是一个函数,那么f1 := Fun也是可以的,那么f1也是一个函数,如果直接调用f1(),那么运行的就是Fun()函数。那么我们就先通过ValueOf()来获取函数的反射对象,可以判断它的Kind,是一个func,那么就可以执行Call()进行函数的调用。示例代码:package main import ( "fmt" "reflect" ) func main() { //函数的反射 f1 := fun1 value := reflect.ValueOf(f1) fmt.Printf("Kind : %s , Type : %s\n",value.Kind(),value.Type()) //Kind : func , Type : func() value2 := reflect.ValueOf(fun2) fmt.Printf("Kind : %s , Type : %s\n",value2.Kind(),value2.Type()) //Kind : func , Type : func(int, string) //通过反射调用函数 value.Call(nil) value2.Call([]reflect.Value{reflect.ValueOf(100),reflect.ValueOf("hello")}) } func fun1(){ fmt.Println("我是函数fun1(),无参的。。") } func fun2(i int, s string){ fmt.Println("我是函数fun2(),有参数。。",i,s) } 说明 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理 reflect.Value.MethodByName这个MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。 []reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。 reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value.Kind不是一个方法,那么将直接panic。 本来可以用对象访问方法直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call 本文参照:http://www.sohu.com/a/313420275_657921https://studygolang.com/articles/12348?fr=sidebarhttp://c.biancheng.net/golang/源代码:https://github.com/rubyhan1314/go_reflect
2022年03月02日
99 阅读
0 评论
0 点赞