目录
背景
基础
helloworld
变量
常量
数据类型
基本数据类型与复杂数据类型
值类型与引用类型
查看变量类型
字符与字符串
类型转换
指针
打包
读取控制台数据
for-range遍历
生成随机数
函数
普通函数
匿名函数
闭包
defer
分配内存
异常捕获
数组
切片
映射
面向对象
结构体
方法
工厂模式
继承
接口
类型断言
文件操作
打开与关闭
读取文件
创建与写入
命令行参数
序列化反序列化
序列化
反序列化
单元测试
并发编程
协程goroutine
MPG模式
全局互斥锁
管道
异常捕获
反射
反射基本数据类型
反射结构体
反射改基本数据类型变量的值
获取结构体所有属性和标签
调用结构体方法
修改结构体字段值
反射构造结构体变量
网络编程
go连接redis
go连接mysql
结语
背景
这里整理一下上个月学习go语言的笔记,更多请参考go的官方文档:https://studygolang.com/pkgdoc
使用的文本编辑器:VS code
基础
项目工程的路径为:D:\develop\Go\workspace,要先把这个路径添加到环境变量GO_PATH里。
代码放在项目路径下的src\go_code\src\project01目录里
helloworld
go语言的helloWorld如下
package main // 必须打main包
import "fmt"
func main()
"szc")
}
main函数必须在main目录下,包名则必须和上级目录名一致(main);一个项目必须有且只有一个main目录(或main包)
go程序既可以直接运行
亦可以先编译,再运行
变量
三种声明方式
一次声明多个变量,变量名和值一一对应
var a, sex, b = 1, "male", 7
也可以这样
a, sex, b := 2, "male", 4
函数外声明全局变量
变量声明后必须使用,而且不能隐式改变类型(int转float)
常量
常量必须赋初值,而且不可更改
常量只能修饰布尔、数值、字符串类型
也可以这么声明常量,可以在函数里面声明
上面b和c可以不写= iota,但是a必须写
数据类型
基本数据类型与复杂数据类型
基本数据类型:
数值型:
1、整数类型(int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、byte)
2、浮点类型(float32、float64)
3、复数(complex64、complex128)
字符型:没有专门的字符型,使用byte保存单个字母字符
布尔型、字符串
数值型中的int32又称为rune,可保存一个unicode码点。int和uint的大小和操作系统位数一样,32位OS则为4字节,64位OS则为8字节。浮点数默认64位,整数默认int。
复杂数据类型:
指针、数组、结构体、管道、函数、切片、接口、映射
值类型与引用类型
值类型:基本数据类型、数组、结构体。变量直接存储值,通常存储于栈中,函数传参时使用值传递
引用类型:指针、切片、映射、管道、接口等。变量存储的是值的地址,通常存储于堆中,会发生GC,函数传参时使用引用传递。
查看变量类型
查看变量类型:
查看变量占用内存大小时,先导入unsafe和fmt包
再调用unsafe.Sizeof函数就行
fmt.Printf("a占用内存大小:%d, sex占用内存大小:%d", unsafe.Sizeof(a), unsafe.Sizeof(sex))
输出结果:
字符与字符串
输出字符时,需要格式化输出,否则会输出的它的ascii值
输出如下
输出汉字和对应unicode码值
结果如下
跨行字符串,用`包住
多行拼接字符串,要在+后面换行,而不是字符串后面
类型转换
不同数据类型之间必须显式类型转换
如果范围大转换成范围小的,可能会发生精度损失,以下是例子:
基本数据类型转string:
%v表示按默认格式输出,%t表示按布尔值输出
也可以用strconv包中的函数进行转换。用之前先导入strconv包
然后调用函数进行转换
string转基本类型:
也是用strconv包中的Parse方法,但Parse方法会返回两个值:转换的值,以及转换错误
得到的输出如下
如果待转换的string不合法,就会转换成对应类型的默认值(0)
指针
和C里面的指针类似
%x表示十六进制,输出如下
同样,通过指针改变变量的值也是一样
打包
包名和目录名一致。
文件中变量、函数名首字母大写,则为public,小写则为包私有
引用自己的包时,先把src目录的上级目录加入环境变量GO_PATH中,然后引入包在src目录下的相对路径
然后就可以引用model包下首字母大写的变量或函数了
fmt.Printf(model.Name)
model包下的test_model.go内容如下所示,文件不用引用。引用目录就行
读取控制台数据
调用fmt.Scan等方法即可
或者指定输入格式
输入时按空格或回车区分即可
for-range遍历
这是一种同时获取索引和值或键值的遍历方式
输出如下
生成随机数
导入math/random和time包
设置种子,生成随机数
函数
普通函数
函数定义,func 函数名(参数列表) 返回值类型 {undefined
函数体
},如下所示
调用如下
fmt.Println(generateRandom(time.Now().Unix(), 100))
init函数,用来初始化源文件
源文件执行流程:全局变量定义->init->main,如果此文件还引入了别的文件,就先执行被引用文件的变量定义和init
匿名函数
匿名函数,没有名字的函数,如下
}后面的(2, 8)表示调用并传参
也可以把匿名函数赋给一个变量
然后就可以对a进行多次调用了
也可以把匿名函数定义成全局变量
闭包
函数和引用环境的整体叫做闭包,例如
AddUpper()返回的匿名函数,引用了匿名函数外的n,所以AddUpper内部形成了闭包。AddUpper的调用如下:
由于形成了匿名函数+外部引用的形式,所以每次调用AddUpper()时,n都会继承上一次调用的值。
就当n是AddUpper()的属性,一个对象只会初始化一次。
defer
defer用来表示一条语句在函数结束后再执行,defer语句会把语句和相应数值的拷贝进行压栈,先入后出。以如下代码为例,这是一个defer + 闭包的例子,makeSuffix的入参为suffix,而返回值是一个函数,此函数入参类型为string,返回值类型也是string。
输出信息如下
可见虽然匿名函数执行了两次,但闭包函数makeSuffix里的语句只执行了一次,而且defer语句先定义的后输出,且都在函数体执行完之后。
分配内存
值类型的用new,返回的是一个指针
输出如下
引用类型的用make
异常捕获
defer、recover捕获异常,相当于try-catch
输出如下
当我们需要自定义错误时,使用errors.New。遇到错误终止程序,使用panic()函数,示例如下
要先导入errors包
输出如下
数组
定义和使用如下所示
数组初始化:元素值默认为0,也可以用下面的方式初始化
由于函数调用时数组形参的值传递,我们可以使用数组指针来实现数组内容在函数里的实际改变,如下所示
输出如下
切片
切片就是动态数组,是数组的一个引用。
切片内存结构相当于一个结构体,由三部分构成:引用数组部分的首地址、切片长度和切片容量
由于是引用,所以改变切片的值,也会改变原数组的对应值
除了引用创建好的数组外,也可以通过make函数来创建切片,传入切片类型、长度和容量
slice0 := make([]int, 4, 10)
显然,make方法创建切片时,会在底层创建一个数组,只是这个数组是我们不可见的
也可以通过类似创建数组的方式创建切片,只是不用传入长度
slice2 := []int{1, 2, 4}
slice可以通过append的方式来进行动态追加,append时底层会构建一个新的数组,把所有要装进去的元素装进去,然后返回。
切片的拷贝可以通过copy函数实现
copy时,dest切片的长度并不重要
删除切片可以通过切片的再切片来得以实现,以下是删除Employees切片中下标为target_index的元素
employees.Employees = append(employees.Employees[: target_index], employees.Employees[target_index + 1:]...) // 后面的...不能省略
映射
键值映射,要先make申请内存,再使用
按键取值
删除某值
delete(m1, "age") // 如果不存在age键,则也不会报错
如果需要清空映射,直接分配新的内存就行
遍历映射,使用for-range
切片同样适用于映射
而映射在函数传参时是引用传递的
面向对象
结构体
结构体是go面向对象的实现方式,没有this指针、没有方法覆写、没有extends关键字等
其声明和使用如下所示
结构体是值类型,因此函数传参是值传递,而且拷贝也是浅拷贝
结构体指针声明和使用如下
如果结构体有切片、映射等属性,也要先分配内存再使用
结构体地址为首字段地址,且内部字段在内存中的地址连续分配。举例如下
则以下代码
的输出如下
0xc00000e460 0xc00000e460 0xc00000e468 0xc00000e470 0xc00000e478
当然,结构体内变量值不一定连续分配,看以下示例
则以下代码
的输出如下
给结构体取别名,相当于定义新的数据类型,两者的变量赋值时,必须强转。
给结构体属性取标签,可以方便转json时转换大小写
输出如下
{"name":"szc","age":23,"homeTown":"Washington"}
方法
go中的方法定义如下
调用方法如下
输出如下
绑定方法时的p是实际调用者的副本,方法调用时会发生值拷贝。所以当结构体有引用型成员变量时,在方法里发生的修改会同步到方法外面
会得到以下输出,age没有变,但映射属性却发生了改变
对应的map变量的值也会发生变化
fmt.Println(m0, "\n", m2)
输出如下
不过,为了能使方法里的修改更高效地同步到外面,声明方法时一般会绑定结构体指针,如下
调用时,还是可以直接使用变量名调用方法,而不必取址
输出如下
所以,方法里对结构体变量的成员进行的修改能不能同步到外面,关键要看方法绑定时绑定的是不是指针,而不是调用时用什么调用的。
以上的方法定义也适用于系统自带类型,定义方法如下
调用过程如下
会得到输出14
如果要实现类似java里的toString,我们可以对指定数据类型绑定String()方法,返回string
然后使用fmt输出Person变量
得到的输出如下
如果String()方法绑定的是结构体指针,那么输出时要传入地址,否则会按照原来的方式输出
会得下面的输出
工厂模式
当我们的结构体首字母小写时,我们可以采取对外暴露一个函数,返回结构体变量指针,来进行结构体变量的构造与访问
然后在main包里进行如下调用
会得到以下输出
{Jason 23}
访问包私有属性也是同样的方法,暴露公有的方法,返回私有的属性
外部进行如下调用
fmt.Println(student0.GetAge())
输出为23
这就是go语言里的工厂模式
继承
继承可以通过嵌套匿名结构体来实现,如下
外部调用如下
graduate0.Name是graduate0.Student.Name的简写,但由于Student在Graduate里是匿名结构体,所以可以省略。此时匿名结构体就相当于父类,外层结构体相当于子类。所以,如果匿名结构体和外层结构体中有同名字段或方法时,默认使用外层结构体的字段或方法,如果要访问匿名结构体中的字段或方法,就要显式调用,如下所示
得到的输出如下
当结构体嵌入了多个匿名结构体,并且这些匿名结构体拥有同名字段或方法时,访问时就必须显式调用了。
如果把匿名结构体改成有名结构体,那么这个有名结构体就相当于外层结构体的属性,访问其属性或方法就必须显式调用。
会得到如下输出
接口
go中的接口定义如下
然后定义两个结构体,来实现ICalculate
再定义一个结构体,为其绑定一个方法,传入接口对象
最后,调用E中的方法
会得到如下输出
go中,只要一个结构体实现了接口的全部方法,这个结构体就是这个接口的一个实现。所以go中没有implement关键字
不过,go中接口变量可以指向接口实现结构体的变量,如下所示
不止结构体,自定义类型都可以实现接口
调用方法也是一样的
空接口里没有任何方法,所以任何类型都实现了空接口
使用接口数组,是实现多态的一种方式
接口应用实例:结构体切片排序,要实现sort包下Interface接口中Len()、Less()、Swap()三个接口
调用时,先导入sort包,再进行调用
输出如下
[{1} {2} {3} {4} {5}]
类型断言
类型断言用来判断某个接口对象是不是某个接口实现的实例
也可以使用switch语句
文件操作
打开与关闭
文件在go中是一个结构体,它的定义和相关函数在os包中,所以要先导包
打开文件和关闭文件的方法如下
其中file的输出如下,可以看到file结构体里存放着一个指针
file = {0xc000110780}
如果指定文件不存在,那么打开文件时会返回如下的错误
open file error = open D:/output00.txt: The system cannot find the file specified.
读取文件
文件读取方法如下所示
要先导包
如果文件不大,就可以使用io/ioutil包下的ReadFile函数一次性读取
导包如下
创建与写入
创建文件并写入内容的方法如下
如果打开已存在的文件,覆写新内容,就要把模式换成os.O_TRUNC
如果是在已存在文件中追加内容,就把模式换成os.O_APPEND
如果既要读文件又要写文件,就要把模式改成os.O_RDWR
目录
背景
基础
helloworld
变量
常量
数据类型
基本数据类型与复杂数据类型
值类型与引用类型
查看变量类型
字符与字符串
类型转换
指针
打包
读取控制台数据
for-range遍历
生成随机数
函数
普通函数
匿名函数
闭包
defer
分配内存
异常捕获
数组
切片
映射
面向对象
结构体
方法
工厂模式
继承
接口
类型断言
文件操作
打开与关闭
读取文件
创建与写入
命令行参数
序列化反序列化
序列化
反序列化
单元测试
并发编程
协程goroutine
MPG模式
全局互斥锁
管道
异常捕获
反射
反射基本数据类型
反射结构体
反射改基本数据类型变量的值
获取结构体所有属性和标签
调用结构体方法
修改结构体字段值
反射构造结构体变量
网络编程
go连接redis
go连接mysql
结语
背景
这里整理一下上个月学习go语言的笔记,更多请参考go的官方文档:https://studygolang.com/pkgdoc
使用的文本编辑器:VS code
基础
项目工程的路径为:D:\develop\Go\workspace,要先把这个路径添加到环境变量GO_PATH里。
代码放在项目路径下的src\go_code\src\project01目录里
helloworld
go语言的helloWorld如下
package main // 必须打main包
import "fmt"
func main()
"szc")
}
main函数必须在main目录下,包名则必须和上级目录名一致(main);一个项目必须有且只有一个main目录(或main包)
go程序既可以直接运行
亦可以先编译,再运行
变量
三种声明方式
一次声明多个变量,变量名和值一一对应
var a, sex, b = 1, "male", 7
也可以这样
a, sex, b := 2, "male", 4
函数外声明全局变量
变量声明后必须使用,而且不能隐式改变类型(int转float)
常量
常量必须赋初值,而且不可更改
常量只能修饰布尔、数值、字符串类型
也可以这么声明常量,可以在函数里面声明
上面b和c可以不写= iota,但是a必须写
数据类型
基本数据类型与复杂数据类型
基本数据类型:
数值型:
1、整数类型(int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、byte)
2、浮点类型(float32、float64)
3、复数(complex64、complex128)
字符型:没有专门的字符型,使用byte保存单个字母字符
布尔型、字符串
数值型中的int32又称为rune,可保存一个unicode码点。int和uint的大小和操作系统位数一样,32位OS则为4字节,64位OS则为8字节。浮点数默认64位,整数默认int。
复杂数据类型:
指针、数组、结构体、管道、函数、切片、接口、映射
值类型与引用类型
值类型:基本数据类型、数组、结构体。变量直接存储值,通常存储于栈中,函数传参时使用值传递
引用类型:指针、切片、映射、管道、接口等。变量存储的是值的地址,通常存储于堆中,会发生GC,函数传参时使用引用传递。
查看变量类型
查看变量类型:
查看变量占用内存大小时,先导入unsafe和fmt包
再调用unsafe.Sizeof函数就行
fmt.Printf("a占用内存大小:%d, sex占用内存大小:%d", unsafe.Sizeof(a), unsafe.Sizeof(sex))
输出结果:
字符与字符串
输出字符时,需要格式化输出,否则会输出的它的ascii值
输出如下
输出汉字和对应unicode码值
结果如下
跨行字符串,用`包住
多行拼接字符串,要在+后面换行,而不是字符串后面
类型转换
不同数据类型之间必须显式类型转换
如果范围大转换成范围小的,可能会发生精度损失,以下是例子:
基本数据类型转string:
%v表示按默认格式输出,%t表示按布尔值输出
也可以用strconv包中的函数进行转换。用之前先导入strconv包
然后调用函数进行转换
string转基本类型:
也是用strconv包中的Parse方法,但Parse方法会返回两个值:转换的值,以及转换错误
得到的输出如下
如果待转换的string不合法,就会转换成对应类型的默认值(0)
指针
和C里面的指针类似
%x表示十六进制,输出如下
同样,通过指针改变变量的值也是一样
打包
包名和目录名一致。
文件中变量、函数名首字母大写,则为public,小写则为包私有
引用自己的包时,先把src目录的上级目录加入环境变量GO_PATH中,然后引入包在src目录下的相对路径
然后就可以引用model包下首字母大写的变量或函数了
fmt.Printf(model.Name)
model包下的test_model.go内容如下所示,文件不用引用。引用目录就行
读取控制台数据
调用fmt.Scan等方法即可
或者指定输入格式
输入时按空格或回车区分即可
for-range遍历
这是一种同时获取索引和值或键值的遍历方式
输出如下
生成随机数
导入math/random和time包
设置种子,生成随机数
函数
普通函数
函数定义,func 函数名(参数列表) 返回值类型 {undefined
函数体
},如下所示
调用如下
fmt.Println(generateRandom(time.Now().Unix(), 100))
init函数,用来初始化源文件
源文件执行流程:全局变量定义->init->main,如果此文件还引入了别的文件,就先执行被引用文件的变量定义和init
匿名函数
匿名函数,没有名字的函数,如下
}后面的(2, 8)表示调用并传参
也可以把匿名函数赋给一个变量
然后就可以对a进行多次调用了
也可以把匿名函数定义成全局变量
闭包
函数和引用环境的整体叫做闭包,例如
AddUpper()返回的匿名函数,引用了匿名函数外的n,所以AddUpper内部形成了闭包。AddUpper的调用如下:
由于形成了匿名函数+外部引用的形式,所以每次调用AddUpper()时,n都会继承上一次调用的值。
就当n是AddUpper()的属性,一个对象只会初始化一次。
defer
defer用来表示一条语句在函数结束后再执行,defer语句会把语句和相应数值的拷贝进行压栈,先入后出。以如下代码为例,这是一个defer + 闭包的例子,makeSuffix的入参为suffix,而返回值是一个函数,此函数入参类型为string,返回值类型也是string。
输出信息如下
可见虽然匿名函数执行了两次,但闭包函数makeSuffix里的语句只执行了一次,而且defer语句先定义的后输出,且都在函数体执行完之后。
分配内存
值类型的用new,返回的是一个指针
输出如下
引用类型的用make
异常捕获
defer、recover捕获异常,相当于try-catch
输出如下
当我们需要自定义错误时,使用errors.New。遇到错误终止程序,使用panic()函数,示例如下
要先导入errors包
输出如下
数组
定义和使用如下所示
数组初始化:元素值默认为0,也可以用下面的方式初始化
由于函数调用时数组形参的值传递,我们可以使用数组指针来实现数组内容在函数里的实际改变,如下所示
输出如下
切片
切片就是动态数组,是数组的一个引用。
切片内存结构相当于一个结构体,由三部分构成:引用数组部分的首地址、切片长度和切片容量
由于是引用,所以改变切片的值,也会改变原数组的对应值
除了引用创建好的数组外,也可以通过make函数来创建切片,传入切片类型、长度和容量
slice0 := make([]int, 4, 10)
显然,make方法创建切片时,会在底层创建一个数组,只是这个数组是我们不可见的
也可以通过类似创建数组的方式创建切片,只是不用传入长度
slice2 := []int{1, 2, 4}
slice可以通过append的方式来进行动态追加,append时底层会构建一个新的数组,把所有要装进去的元素装进去,然后返回。
切片的拷贝可以通过copy函数实现
copy时,dest切片的长度并不重要
删除切片可以通过切片的再切片来得以实现,以下是删除Employees切片中下标为target_index的元素
employees.Employees = append(employees.Employees[: target_index], employees.Employees[target_index + 1:]...) // 后面的...不能省略
映射
键值映射,要先make申请内存,再使用
按键取值
删除某值
delete(m1, "age") // 如果不存在age键,则也不会报错
如果需要清空映射,直接分配新的内存就行
遍历映射,使用for-range
切片同样适用于映射
而映射在函数传参时是引用传递的
面向对象
结构体
结构体是go面向对象的实现方式,没有this指针、没有方法覆写、没有extends关键字等
其声明和使用如下所示
结构体是值类型,因此函数传参是值传递,而且拷贝也是浅拷贝
结构体指针声明和使用如下
如果结构体有切片、映射等属性,也要先分配内存再使用
结构体地址为首字段地址,且内部字段在内存中的地址连续分配。举例如下
则以下代码
的输出如下
0xc00000e460 0xc00000e460 0xc00000e468 0xc00000e470 0xc00000e478
当然,结构体内变量值不一定连续分配,看以下示例
则以下代码
的输出如下
给结构体取别名,相当于定义新的数据类型,两者的变量赋值时,必须强转。
给结构体属性取标签,可以方便转json时转换大小写
输出如下
{"name":"szc","age":23,"homeTown":"Washington"}
方法
go中的方法定义如下
调用方法如下
输出如下
绑定方法时的p是实际调用者的副本,方法调用时会发生值拷贝。所以当结构体有引用型成员变量时,在方法里发生的修改会同步到方法外面
会得到以下输出,age没有变,但映射属性却发生了改变
对应的map变量的值也会发生变化
fmt.Println(m0, "\n", m2)
输出如下
不过,为了能使方法里的修改更高效地同步到外面,声明方法时一般会绑定结构体指针,如下
调用时,还是可以直接使用变量名调用方法,而不必取址
输出如下
所以,方法里对结构体变量的成员进行的修改能不能同步到外面,关键要看方法绑定时绑定的是不是指针,而不是调用时用什么调用的。
以上的方法定义也适用于系统自带类型,定义方法如下
调用过程如下
会得到输出14
如果要实现类似java里的toString,我们可以对指定数据类型绑定String()方法,返回string
然后使用fmt输出Person变量
得到的输出如下
如果String()方法绑定的是结构体指针,那么输出时要传入地址,否则会按照原来的方式输出
会得下面的输出
工厂模式
当我们的结构体首字母小写时,我们可以采取对外暴露一个函数,返回结构体变量指针,来进行结构体变量的构造与访问
然后在main包里进行如下调用
会得到以下输出
{Jason 23}
访问包私有属性也是同样的方法,暴露公有的方法,返回私有的属性
外部进行如下调用
fmt.Println(student0.GetAge())
输出为23
这就是go语言里的工厂模式
继承
继承可以通过嵌套匿名结构体来实现,如下
外部调用如下
graduate0.Name是graduate0.Student.Name的简写,但由于Student在Graduate里是匿名结构体,所以可以省略。此时匿名结构体就相当于父类,外层结构体相当于子类。所以,如果匿名结构体和外层结构体中有同名字段或方法时,默认使用外层结构体的字段或方法,如果要访问匿名结构体中的字段或方法,就要显式调用,如下所示
得到的输出如下
当结构体嵌入了多个匿名结构体,并且这些匿名结构体拥有同名字段或方法时,访问时就必须显式调用了。
如果把匿名结构体改成有名结构体,那么这个有名结构体就相当于外层结构体的属性,访问其属性或方法就必须显式调用。
会得到如下输出
接口
go中的接口定义如下
然后定义两个结构体,来实现ICalculate
再定义一个结构体,为其绑定一个方法,传入接口对象
最后,调用E中的方法
会得到如下输出
go中,只要一个结构体实现了接口的全部方法,这个结构体就是这个接口的一个实现。所以go中没有implement关键字
不过,go中接口变量可以指向接口实现结构体的变量,如下所示
不止结构体,自定义类型都可以实现接口
调用方法也是一样的
空接口里没有任何方法,所以任何类型都实现了空接口
使用接口数组,是实现多态的一种方式
接口应用实例:结构体切片排序,要实现sort包下Interface接口中Len()、Less()、Swap()三个接口
调用时,先导入sort包,再进行调用
输出如下
[{1} {2} {3} {4} {5}]
类型断言
类型断言用来判断某个接口对象是不是某个接口实现的实例
也可以使用switch语句
文件操作
打开与关闭
文件在go中是一个结构体,它的定义和相关函数在os包中,所以要先导包
打开文件和关闭文件的方法如下
其中file的输出如下,可以看到file结构体里存放着一个指针
file = {0xc000110780}
如果指定文件不存在,那么打开文件时会返回如下的错误
open file error = open D:/output00.txt: The system cannot find the file specified.
读取文件
文件读取方法如下所示
要先导包
如果文件不大,就可以使用io/ioutil包下的ReadFile函数一次性读取
导包如下
创建与写入
创建文件并写入内容的方法如下
如果打开已存在的文件,覆写新内容,就要把模式换成os.O_TRUNC
如果是在已存在文件中追加内容,就把模式换成os.O_APPEND
如果既要读文件又要写文件,就要把模式改成os.O_RDWR