无论什么知识,在官网肯定能找到:https://studygolang.com/pkgdoc
然而这些并不影响Go越来越火, 如果一点要对比Java和Go哪个好, 我觉得不如都会, 这样更好~
我们对 .go 文件使用go build XXX.go的时候就会帮我们编译生成一个XXX.exe的二进制可执行文件 ,也可以直接使用 go run 执行一个go文件,但是慢
说明:两种执行流程的方式区别
1)如果我们先编译生成了可执行文件/二进制文件,那么我们可以将该可执行文件拷贝到没有go开发环境的机器上,仍然可以运行
2)如果我们是直接go run go源代码,那么如果要在另外一个机器上这么运行,也需要go开发环境,否则无法执行。
3)使用go build编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多。
Go支持的数据类型整体分为俩大类,有接班数据类型和复杂数据类型,每一种数据类型都会分配大小不同的内存空间
go语言的字符串的字节使用utf-8编码标识的Unicode文本,字符串一旦赋值就不能修改,所以go字符串是不可变的
const (
a = 0
b = 1
c = 2
d = 3
)
const (
a = iota
b
c
d
)
表示给a赋值为0, b在a的基础上+1, c在b的基础上+1, 这种写法就比较专业了
3. Go的常量写法一般使用驼峰形式
4. 仍然通过首字母的大小写来控制常量的访问范围
这里的指针和C语言的指针是一个意思,如果学过C语言,那很好理解, 指针本身有一个它自己地址,而它自己的地址上所存储的数据也是一个地址,这个地址指向的空间中存的才是真正的数据值
func main() {
var i = 10
fmt.Println("num的地址", &num)
var ptr *int
ptr = &i
fmt.Println("ptr的地址", &ptr)
fmt.Println("ptr的数据", ptr)
fmt.Println("ptr的数据表示的真实意义", *ptr)
}
i 的地址 0xc0420e080
ptr的地址 0x04206c020
ptr的数据 0xc0420e080
ptr的数据表示的真实意义 10
所以说指针本质上存储的是一个地址,而存储的这个地址所存储的才 是真正的数据
func main() {
var ptr *float64
var num = 10
ptr = &num // error 还要注意指针也是有类型的
}
常见的值类型和引用类型:
1)值类型:基本数据类型int系列, float系列, bool, string 、数组和结构体struct
2)引用类型:指针、slice切片、map、管道chan、interface 等都是引用类型
值类型的变量直接存储值,通常在栈中分配
引用类型, 变量存储的是一个地址,这个地址对应的空间才真正存储数据,内存通常在堆上进行分配空间,当没有任何变量引用这个地址的时候,就会对其进行GC回收
很简单,看个例子就行
func main() {
// 例一
nums := [4]int{1,2,3,4}
for i := 0; i < 4; i++ {
fmt.Println(nums[i])
}
// 例二
i := 0
for {
if nums[i] == 3 {
break
}
i++
}
}
你可以使用break关键字来退出for循环。break关键字同时也允许你创建一个没有像i <100的这样的终止条件的for循环,因为终止条件可以放在for循环内部的代码块中。在for循环中允许有多个退出循环的地方。
此外,你可以使用continue关键字在for循环中跳过一次循环。continue关键字会停止执行当前的代码块,跳回顶部的for循环,然后继续进行下一次循环。
Go同时提供了range关键字,它能配合for循环以帮助你在遍历Go的数据类型时写出更易于理解的代码。
range关键字的主要优势在于并不需要知道一个切片或者字典的长度就可以一个接一个的对它们的元素进行操作。稍后你会看到range的示例。
对一个数组使用range关键字会返回两个值:一个是数组的索引,一个是该索引上的值。你可以两个值都使用,或者使用其中一个,甚至如果你只是想单纯的计算一下数组的元素个数的话,也可以两个值都不使用。
for index, value := range nums {
fmt.Println("index: ", index, "value: ", value)
}
index: 0 value: 1
index: 1 value: 2
index: 2 value: 3
index: 3 value: 4
for range方式在使用channel和map的时候也经常使用
parts := map[string]string{}
parts = make(map[string]string)
parts["a"] = "aa"
parts["b"] = "bb"
parts["c"] = "cc"
for k, v := range parts {
fmt.Println("k:", k, "v:", v)
}
k: a v: aa
k: b v: bb
k: c v: cc
和其他语言类似,但也有几点不同,比如:
func main() {
var score int
_, _ = fmt.Scan(&score)
switch {
case score > 90:
fmt.Println("A")
case score > 80:
fmt.Println("B")
case score > 70:
fmt.Println("C")
default:
fmt.Println("D")
}
}
func main() {
var num = 10
var name = "name"
findType(num)
findType(name)
}
func findType(a interface{}) {
switch a.(type) {
case string:
fmt.Println("string")
case int64:
fmt.Println("int64")
case int:
fmt.Println("int")
case byte:
fmt.Println("byte")
case bool:
fmt.Println("bool")
default:
}
}
func test(name string) {
switch name {
case "a", "x":
fallthrough
case "b":
fmt.Println("b")
default:
}
}
和C语言类似, 所有的函数都是全局函数,可以被项目中所有文件使用, 但是区分可导出还是不可导出来控制访问权限,而且在项目中函数名也是唯一的
函数定义
func 函数名(函数参数列表){
代码体/ 程序体/ 函数体
}
变量名数据类型如果有多个参数中间用逗号分隔/先定义后调用
func add(a, b int) {
sum := a + b
fmt.Println(sum)
}
func main() {
a1 := 10
b1 := 20
add(a1, b1)
}
func sum(nums ...int) {
sum := 0
for i := 0; i < len(nums); i++ {
sum += nums[i]
}
fmt.Println(sum)
}
func sum1(arr ...int) {
sum := 0
for i, data := range arr {
fmt.Println("下标:", i)
sum += data
}
fmt.Println(sum)
}
func main11() {
sum(1,2,3)
sum(4,5,6,7,89,0)
}
这种不定参数函数,其参数其实本质是切片, 如果使用可变参数,必须放到最后一个参数上。
func Test(nums ...int) {
var s []int
s = nums
s = append(s, 1)
}
如您已经从strconv.atoi() 等函数所知,Go函数可以返回多个不同的值,这就避免了您必须创建一个专用的结构,以便能够同时从一个函数接收多个值。您可以声明一个如下返回四个值的函数,两个int值,一个float64值和一个string :
func aFunction() (int, int, float64, string){
return 1, 1, 1.0, "a"
}
使用函数的时候, 自然有需要使用多个变量去接受返回值
func main() {
m, _, f, s := aFunction()
fmt.Println(m, f, s)
}
和C不同,Go可以命名函数的返回值。另外,当一个函数有个没有任何参数的return语句时,函数自动返回每个命名的返回值的当前值,返回顺序与函数定义中声明的顺序相同。
func namedMinMax(x, y int) (min, max int) {
if x > y {
min = y
max = x
} else {
min = x
max = y
}
return
}
在这段代码中,您可以看到namedMinMax()函数的实现,它使用了命名返回值参数。然而,有一点:namedMinMax()函数没有明确的返回任何变量在 return语句。不过,由于这个函数命令了返回值,min和max被自动返回,以它们被定义的顺序。
当然在return语句也带上返回值, 这样返回值就会在return那里最后被刷新一次数据
func minMax(x, y int) (min, max int) {
if x > y {
min = y
max = x
} else {
min = x
max = y
}
return min, max
}
我们知道Go函数中有个defer函数, 他是在父函数执行结束的时候立即去执行这个defer定义的函数,那么如果在defer中修改了被命名的返回值,会不会生效呢?
// 这种不会修改源数据->
func Test1() (a int , b float32) {
defer func(a int, b float32) {
a = 99
b = 99.9
}(a, b)
a = 10
b = 2.1
return a + 1, b + 2
}
// 这种会修改源数据
func Test2() (a int , b float32) {
defer func() {
a = 99
b = 99.9
}()
a = 10
b = 2.1
return a + 1, b + 2
}
func main() {
fmt.Println(Test1())
fmt.Println(Test2())
}
11 4.1
99 99.9
所以结论就是如果defer函数执行的时候有自己创建局部变量,那自然不会修改源数据, 如果defer函数还是使用的父函数栈区的局部变量,那定会改变源数据
变量的使用范围,如果程序中出现相同的变量名,优先使用自己内部的,如果自己内部没有才会向外面去找,使用就近原则
和C语言一样, 在被调用函数里修改值传递的数据可以改变原数据
func Swap(a, b *int) {
*a, *b = *b ,*a
fmt.Println(*a, *b)
}
func main() {
a := 10
b := 20
fmt.Println(a, b)
Swap(&a, &b)
fmt.Println(a, b)
}
10 20
20 10
20 10
通过type关键字定义的函数类型其实是规定一个变量, 这个变量只能指定该类型格式的一个函数
// type可以定义类型, 也可以给已存在的类型起别名
type fType func(a , b * int)
type fType1 func(a, b int) int
type fType2 func(a, b, c int) (int, string)
type fType3 int
// type可以定义类型, 也可以给已存在的类型起别名
type fType func(a , b * int)
type fType1 func(a, b int) int
type fType2 func(a, b, c int) (int, string)
type fType3 int
func test1(a, b *int) {
fmt.Println("test1")
}
func test2(a, b int) int {
fmt.Println("test2")
return 0
}
func test3(a, b, c int) (int, string) {
fmt.Println("test3")
return 0 , "test3"
}
func test4(a, b, c int) (int, string) {
fmt.Println("test4")
return 0, "test4"
}
func main() {
a := 1
b := 2
c := 3
var f fType
f = test1
f(&a, &b)
f1 := test2
_ = f1(a, b)
var f2 fType2
f2 = test3
_, _ = f2(a, b, c)
f2 = test4
_, _ = f2(a, b, c)
}
test1
test2
test3
test4
所以函数也可以使用 := 进行类型推导
func swap(a *int, b *int) {
fmt.Println(a)
c := a // 此时c也是指针类型
fmt.Printf("%T, %v\n", c, c)
*a = *b
*b = *c
}
func main() {
f := swap // 函数类型推导
fmt.Printf("%T, %v\n", f, f)
var aa = 10
var b = 20
fmt.Println(aa, b)
f(&aa, &b)
fmt.Println(aa, b)
}
func(*int, *int), 0x6bdd60
10 20
0xc00000a098
*int, 0xc00000a098
20 20
首先看一下Go程序的内存分布
一般基本数据类型会存储在对应函数的栈区, 引用数据类型一般分配到堆区进行存储, 代码区会存储代码段
每调用一个函数,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他栈的空间区分开来
当一个函数调用结束就会回收这栈空间
在Go中基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
Go函数不支持重载。
go中函数也是一个数据类型,所以可以将函数作为一个形参进行传递执行
Go函数可以接收另一个Go函数作为参数,这个功能为您用Go函数增加了非常广泛的用途。这个功能最为常用的用途就是闭包。
func fun1(a int) int {
fmt.Println("a + a")
return a +a
}
func fun2(a, b int) int {
fmt.Println("a * b")
return a * b
}
func fun3(f1 func(a int) int, f2 func(a, b int) int, a, b int) int {
return f1(a) + f2(a, b)
}
func main() {
f1 := fun1 // 这里是类型推导,并不是函数调用
f2 := fun2
fmt.Println(fun3(f1, f2, 1, 2))
// 第二种用法
fmt.Println(fun3(func(a int) int {
return a + a
}, func(a, b int) int {
return a * b
}, 1, 2))
}
尽管这个第二种方法可以运行简单的,小巧的函数参数,对于多行代码的函数还是尽量不要这样使用。
每个源文件都会有一个init函数,该函数会在main执行前执行,被go框架调用,也就是init会在所有函数之前被调用
所以会在init函数中完成初始化操作
所以如果一个源文件同时包含全局变量定义,执行流程是: 变量定义 -》init -》main函数
如果遇到import导包,导的这个包里的init比main里的优先执行
总结: 被导包的全局变量被定义-> 执行被导包的init函数-> main包全局变量被定义-> main包的init函数执行 -> 执行main函数
Go支持匿名函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
func main() {
res := 0
func(a, b int) {
fmt.Println(a + b)
res = a + b
}(10, 20)
fmt.Println(res)
res = func(a, b int) int {
return a + b
}(10 ,20)
fmt.Println(res)
}
f1 := func(a, b int) int {
return a * b
}
fmt.Println(f1(1, 2))
fmt.Println(f1(2, 3))
var (
F1 = func(a, b int) int {
return a / b
}
)
func main() {
fmt.Println(F1(4, 2))
}
基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体) 可以理解为Java的类
func addUpper() func(int) int {
num := 10
return func(i int) int {
num = num + i
return num
}
}
func main() {
f := addUpper() // 注意这里先进行调用addUpper函数, 然后将返回值进行函数类型推导给了f
fmt.Println(f(10))
fmt.Println(f(10))
fmt.Println(f(10))
}
返回的是一个匿名函数,但是这个匿名函数引用到函数外的num ,因此这个匿名函数就和num形成一个整体,构成闭包。
1)编写一个函数makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个闭包
2)调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返
回文件名.jpg,如果已经有.jpg后缀,则返回原文件名。
3)要求使用闭包的方式完成
4)strings.HasSuffix
func makeSuffix(suffix string) func(src string) string {
return func(src string) string {
if !strings.HasSuffix(src, suffix) {
return src + suffix
}
return src
}
}
func main() {
f := makeSuffix(".jpg")
fmt.Println(f("a.jpg"))
fmt.Println(f("b"))
fmt.Println(f("c"))
}
a.jpg
b.jpg
c.jpg
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)。
func sum(a, b int) int {
defer fmt.Println("a=", a)
defer fmt.Println("b=", b)
ret := a + b
fmt.Println("ret=", ret)
return ret
}
func main() {
fmt.Println("sum", sum(10, 20))
}
ret= 30
b= 20
a= 10
sum 30
注意, 将defer修饰的函数入defer栈的时候,会将使用到的数据同时以值拷贝的方式压入栈, 这里使用到的数据只是指在创建defer修饰的函数使用到的数据, 也就是如果是值拷贝类型的数据,在defer中修改后不会影响父函数return的数据
func sum(a, b int) int {
defer func(a int) {
a++
fmt.Println("a=", a)
}(a)
defer func(b int) {
b++
fmt.Println("b=", b)
}(b)
ret := a + b
a++
b++
fmt.Println("ret=", ret)
return ret
}
func main() {
fmt.Println("sum", sum(10, 20))
}
ret= 30
b= 21
a= 11
sum 30
如果是下面这种情况, 就是创建defe修饰的函数时没有使用到外部数据,那么就会造成影响, 但是return数据自然还是不会受影响
func sum(a, b int) int {
defer func() {
a++
fmt.Println("a=", a)
}()
defer func() {
b++
fmt.Println("b=", b)
}()
ret := a + b
a++
b++
fmt.Println("ret=", ret)
return ret
}
func main() {
fmt.Println("sum", sum(10, 20))
}
ret= 30
b= 22
a= 12
sum 30
defer最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。
file, _ := os.Open("D:/a.txt")
defer file.Close()
go为了方便编程,提供了一些函数,可以直接使用,称为go的内置函数
这些内置函数在官网的buildin包下
我做一下简单的代码案例, 都是些常用的
func main() {
// 获取当前时间
now := time.Now()
fmt.Printf("now=%v, now Type=%T\n", now, now)
// 时间戳
fmt.Println("毫秒时间戳", now.Unix())
fmt.Println("纳秒时间戳", now.UnixNano())
// 获取年月日时分秒
fmt.Println("年=", now.Year())
fmt.Println("月=", now.Month())
fmt.Println("日=", now.Day())
fmt.Println("时=", now.Hour())
fmt.Println("分=", now.Minute())
fmt.Println("秒=", now.Second())
// 格式化日期
fmt.Printf("当前年月日%02d-%02d-%02d %02d:%02d:%02d\n",now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), now.Second())
// 还有一种方式
fmt.Println(now.Format("2006/01/02 15:04:05")) // 这个时间格式是固定的
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("15:04:05"))
}
now=2021-12-27 11:25:30.3770708 +0800 CST m=+0.003156301, now Type=time.Time
毫秒时间戳 1640575530
1640575530377070800
年= 2021
月= December
日= 27
时= 11
分= 25
秒= 30
当前年月日2021-12-27 11:25:30
2021/12/27 11:25:30
2021-12-27
11:25:30
"2006/01/02 15:04:05”这个字符串的各个数字是固定的,必须是这样写。
"2006/01/02 15:04:05”这个字符串各个数字可以自由的组合,这样可以按程序需求来返回时间和日期
时间的常量const (
Nanosecond Duration =1//纳秒Microsecond
= 1000* Nanosecond //微秒
Millisecond
= 1000* Microsecond//毫秒
second
= 1000* Millisecond//秒
Minute
=60* Second //分钟
Hour
= 60* Minute l/小时
)
常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到1oo毫秒100 * time.Millisecond
有这么多常量就是为了方便使用,比如你要0.1秒,如果使用second * 0.1就会有问题
1)在默认情况下,当发生错误后(panic),程序就会退出(崩溃.)
2)如果我们希望,当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件.短信。。。
func main() {
defer func() {
err := recover()
if err != nil {
fmt.Println("捕获异常")
fmt.Println(err)
}
}()
num1 := 10
num2 := 0
fmt.Println(num1 / num2)
}
捕获异常
runtime error: integer divide by zero
进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮。
但是一定要注意是在defer修饰的函数里才可以, 如果在defer修饰的函数里再去执行函数中去执行recover是不会捕获到异常的
func main() {
defer func() {
func() {
err := recover()
if err != nil {
fmt.Println("捕获异常")
fmt.Println(err)
}
}()
}()
num1 := 10
num2 := 0
fmt.Println(num1 / num2)
}
上述代码就会直接让程序奔溃, 不会捕获到异常
Go程序中,也支持自定义错误,使用errors.New和 panic 内置函数。
或者使用fmt.errorf()也可以创建一个错误, 其实区别就是fmt.errorf可以formate内部string
func test1() error {
return fmt.Errorf("error")
}
func test2() {
err := test1()
if err != nil {
panic(err)
}
}
这个内容较多, 所有我有单独的博客深入讲或者部分内容,这里我做简单的介绍GO集合大全
一定要注意数组的[3]int和[4]int不是同一个数据类型
func main() {
var arr1 = [3]int{1, 2, 3}
arr2 := [4]int{1,2,3,4}
arr3 := [...]int{1,2,3,4,5}
arr4 := [...]string{1: "b", 2: "c", 0: "a"} // 通过下标定位index
fmt.Println(arr1, arr2, arr3, arr4)
}
[1 2 3] [1 2 3 4] [1 2 3 4 5] [a b c]
// 方式一
for i := 0; i < len(arr1); i++ {
fmt.Println(arr1[i])
}
// 方式二
for index, value := range arr1 {
fmt.Println(index, value)
}
数组使用注意事项和细节
1)数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化。
2)var arr []int这时arr就是一个slice切片,切片后面专门讲解,不急哈.
3)数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。4)数组创建后,如果没有赋值,有默认值数值类型数组:
默认值为0
字符串数组:默认值为""
bool数组:默认值为false
5)使用数组的步骤1.声明数组并开辟空间2给数组各个元素赋值3使用数组6)
数组的下标是从o开始的。
7)数组下标必须在指定范围内使用,否则报panic:数组越界,比如var arr [5]int则有效下标为0-4
8)Go的数组属值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会相互影响
9)如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
记着go中数组也是值拷贝(一不留心就是大坑)
如果想用java那种把数组传递变为引用传递,那就得传递数组的指针
var nums []int
nums = make([]int, 0, 1) // 类型 初始长度 容量, 注意初始长度强烈建议为0
切片的内存布局,切片记着一直是引用传递的,只是在内存不够的时候才会进行开辟新空间然后把旧数据放到新的空间上
比如下面代码
func main() {
arr := [5]int{0,1,2,3,4}
slice1 := arr[0:4] // 这底层就是分割出来一个切片, 采用左闭右的方式进行隔离出数据
fmt.Println("slice", slice1)
fmt.Println("len", len(slice1))
fmt.Println("cap", cap(slice1))
slice1[0] = 99
fmt.Println("arr", arr) // 输出 arr [99 1 2 3 4] 这里看到slice1和arr使用的同一个内存空间
slice1 = append(slice1, 4)
slice1 = append(slice1, 5)
slice1 = append(slice1, 6)
slice1 = append(slice1, 7)
slice1[1] =999
fmt.Println("arr", arr)// 还是输出 arr [99 1 2 3 4] 这里看到slice1和arr使用的同一个内存空间
fmt.Println("slice", slice1) // 输出 slice [99 999 2 3 4 5 6 7]
}
总结:
从上面可以看到
1.slice的确是一个引用类型
2.slice从底层来说,其实就是一个数据结构(struct结构体)
type slice struct {
ptr *[2]int
len int
cap int
}
func main() {
// 第一种方式
arr := [5]int{0, 1, 2, 3, 4}
slice1 := arr[0:4] // 这底层就是分割出来一个切片, 采用左闭右的方式进行隔离出数据
// 第二种方式
slice2 := make([]int, 0, 4)
}
方式1是直接引用数组,这个数组是事先存在的,程序员是可见的。既然可见也是一种风险,如果切片不进行扩容,那修改原数组中的数据会对切片产生影响,这是一个坑
方式2是通过make来创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员是看不见的, 但是创建的时候。make创建切片的示意图:
切片初始化时var slice = arr[startIndex:endIndex] 说明:从arr数组下标为startlndex,取到下标为endIndex的元素(不含arr[endIndex])。
切片初始化时,仍然不能越界。范围在[O-len(arr)]之间,但是可以动态增长
var slice = arr[0:end]可以简写
var slice = arr[:end]
var slice = arr[start:len(arr)]可以简写:
var slice = arr[start:]
var slice = arr[0:len(arr)]
可以简写:var slice = arr[:]
cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。
切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者make一个空间供切片来使用
切片可以继续切片, 切片无论你切几次,只要容量len
用append内置函数,可以对切片进行动态追加
切片append操作的底层原理分析:
1)切片append操作的本质就是对数组扩容
2)go底层会创建一下新的数组newArr(安装扩容后大小)3)将slice原来包含的元素拷贝到新的数组newArr
说明: copy(para1, para2): para1和 para2都是切片类型。将2拷贝给1
使用copy的时候要求俩个参数都是切片,而且拷贝之后俩个切片之间的数据是独立的
注意拷贝时如果容量不够,go默认只会拷贝自己能放下的那点数据,也就是拷贝的数据是俩个切片最小len值
看下面代码输出是什么
a := []int{1,2,3,4,5}
s := make([]int, 1, 4)
fmt.Println(s)
copy(s, a)
fmt.Println(s)
[0]
[1]
输出0是因为设置了输出容量,给了默认值所以第一个位置为0, 输出1的原因是因为进行了拷贝, 然后以l俩个切片len的最小值作为拷贝数据量
func main() {
arr := [5]int{1,2,3,4,5}
var slice []int
slice = arr[:]
var slice2 = slice
slice2[0] = 10
fmt.Println("arr", arr)
fmt.Println("slice", slice)
fmt.Println("slice2", slice2)
}
arr [10 2 3 4 5]
slice [10 2 3 4 5]
slice2 [10 2 3 4 5]
原因就是因为从数组中拷贝切片,只要切片没有发送扩容, 那么就和原数组使用的同一个内存空间
func main() {
slice1 := make([]int, 1, 2)
slice2 := make([]int, 1, 2)
slice3 := make([]int, 1, 2)
test5(slice1)
test6(slice2)
test7(slice3)
fmt.Println("slice1", slice1)
fmt.Println("slice2", slice2)
fmt.Println("slice3", slice3)
}
func test5(slice []int) {
slice[0] = 10
}
func test6(slice []int) {
slice = append(slice, 5)
slice[0] = 10
}
func test7(slice []int) {
slice = append(slice, 5)
slice = append(slice, 5)
slice = append(slice, 5)
slice = append(slice, 5)
slice = append(slice, 5)
slice = append(slice, 5)
slice[0] = 10
}
slice1 [10]
slice2 [10]
slice3 [0]
总的来说原因就是切片虽然说是引用传递,但只是底层数组的引用传递,其len和cap都是值传递, 而且引用传递本质还是引用的一个值拷贝, 所以一旦发生扩容,那么整个切片就和值传递一个样了
string底层是一个byte数组,因此string也可以进行切片处理, 其原理就是因为底层使用byte数组存储了字符串的每一个字符
str := "abcdefg"
slice := str[4:]
fmt.Println(slice)
而这个底层的byte数组是不可见的,所以string也是不可修改的,也就说不能通过str[0]= 'z’方式来修改字符串
如果要修改,可以将字符串转为byte数组之后再进行修改,或者rune数组进行修改
str := "abcdefg"
//slice := str[4:]
//mt.Println(slice)
strArr := []byte(str)
strArr[0] = 'm'
fmt.Println("str:", str, "strArr:", string(strArr)) // 注意这里输出的不一样
我们转成[ ]byte后,可以处理英文和数字,但是不能处理中文
原因是[]byte字节来处理,而一个汉字,是3个字节,因此就会出现乱码
解决方法是将string转成[]rune即可,因为[]rune是按字符处理,兼容汉字
str1 := "我是Listen"
sreArr1 := []rune(str1)
sreArr1[0] = '你'
fmt.Println("strArr1:", string(sreArr1))
map是key-velue数据结构,又称为字段或者关联数组。类似其它编程语言的集合,在编程中是经常使用到。
map是引用类型,这个前面也提到过
key可以是什么类型
golang中的map的 key 可以是很多种类型,比如bool,数组,string,指针,channel ,还可以是只包含前面几个类型的接口,结构体,数组, 但是通常为int 、 string 注意: slice,map还有function不可以,因为这几个没法用==来判断, 至于value可以是任何东西
map声明的举例:
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
上面定义好之后, map必须进行make初始化,不然不能进行使用
注意:声明是不会分配内存的,初始化需要make ,分配内存后才能赋值和使用。
使用make的时候还有设置初始容量
map增加和更新:
map[“key”]= value l/如果key还没有,就是增加,如果key存在就是修改。
map删除:
说明:
delete(map,“key”) ,delete是一个内置函数,如果key存在,就删除该key-value,如果key不存在,不操作,但是也不会报错
细节说明
1)如果我们要删除map的所有key ,没有一个专门的方法进行一次性除,必须逐个删除
2)或者map = make(…),make一个新的,让原来的成为垃圾,被gc回收
func main() {
var kv map[string]string
kv = make(map[string]string, 4)
kv["a"] = "aa"
kv["b"] = "bb"
val, find := kv["b"]
if find { // 是否存在
fmt.Println(val)
}
// 遍历
for k, v := range kv {
fmt.Println("k:", k, "v:", v)
}
fmt.Println("------------------------------------")
// 复杂map的遍历
infos := make(map[int]map[string]string, 4)
_ ,ok := infos[1]
if !ok { // 不存在
infos[1] = make(map[string]string, 2)
}
values := infos[1] // 注意这里要获取make后的map
values["name"] = "listen"
values["age"] = "22"
values["id"] = "001"
_ ,ok = infos[2]
if !ok { // 不存在
infos[2] = make(map[string]string, 2)
}
values = infos[2]
values["name"] = "tom"
values["age"] = "21"
values["id"] = "002"
for _, v := range infos {
for kk, vv := range v {
fmt.Println(kk, ":", vv)
}
}
}
bb
k: a v: aa
k: b v: bb
name : tom age : 21 id : 002 name : listen age : 22 id : 001
基本介绍
切片的数据类型如果是map,则我们称为slice of map,map切片,这样使用则map个数就可以动态变化了。
func main() {
// 申明一个map切片
var stu []map[string]string
stu = make([]map[string]string, 2) // 此时意思是切片里有俩个map引用, 所以也只能存储俩个map
if stu[0] == nil {
stu[0] = make(map[string]string, 2)
stu[0]["name"] = "tom"
stu[0]["age"] = "22"
}
if stu[1] == nil {
stu[1] = make(map[string]string, 2)
stu[1]["name"] = "listen"
stu[1]["age"] = "21"
}
// 但是注意这里有个坑, 如果使用stu[2]就会出现panic, 这里要注意, 所以如果需要再次添加必须使用append
s := map[string]string{
"name":"lisa",
"age":"20",
}
stu = append(stu, s)
fmt.Println(stu)
}
注意代码中注释说明的那个坑就好
type Person struct {
Name string
age int
infos map[string]string
}
func main() {
p1 := Person{
Name: "Listen",
age: 22,
infos: make(map[string]string, 2), // 必须
}
}
如果结构体的字段是指针,slice, map这些初始的时候都是nil, 需要使用make之后才能分配空间才能进行正常使用
var p2 = new(Person) // 一个Person的指针
(*p2).Name = "tom"
p2.age = 21 // 这俩种方式都支持
(*p2).infos = make(map[string]string, 2)
fmt.Println(*p2)
用new关键字出来的结构体是一个指向person的指针, 使用(*p2)和p2都可以直接操作数据, 这是因为go的设计者为了程序员使用方便,底层会对p2.age = 21进行处理, 变成我们第一种方式那样
type A struct {
Name string
}
type B struct {
Name string
}
func main() {
a := A{
Name: "",
}
b := B{
Name: "",
}
b = B(a)
fmt.Println(b)
}
type Cat struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
marshal, _ := json.Marshal(Cat{
Name: "bike",
Age: 0,
})
fmt.Println(string(marshal))
}
{“name”:“bike”,“age”:0}
Golang中的方法是作用在指定的数据类型上的 (即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
方法也会被叫做接收器
type Cat struct {
Name string `json:"name"`
Age int `json:"age"`
}
func (c Cat) eat() {
c.Name = "listen"
fmt.Println(c.Name, ": eating")
}
func main() {
cat := Cat{
Name: "bike",
Age: 0,
}
cat.eat()
fmt.Println(cat) // {bike 0}
test(cat)
fmt.Println(cat) // {bike 0}
}
func test(cat Cat) {
cat.Name = "a"
}
上述写法会有一个问题, 因为go中结构体传参是值拷贝,所以test和eat里的修改不会影响外面,如果想要有影响,那就按照下面这样写
type Cat struct {
Name string `json:"name"`
Age int `json:"age"`
}
func (c *Cat) eat() {
c.Name = "listen"
fmt.Println(c.Name, ": eating")
}
func main() {
cat := Cat{
Name: "bike",
Age: 0,
}
cat.eat()
fmt.Println(cat)
test(&cat)
fmt.Println(cat)
}
func test(cat *Cat) {
cat.Name = "a"
}
而且我们大多数情况下都是使用指针进行函数的传参
1)调用方式不一样
函数的调用方式:函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
2)对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
3)对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
func GetCat(name string, age int) *Cat {
if ...
return &Cat{
Name: "",
Age: 0,
}
}
哈哈哈 大致就这样滴
Go中可以继承,只是通过嵌套匿名结构体的方式实现继承
也就是说:在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
目的就是:
代码复用性提高
扩展性和维护性提高
(复用性和扩展性,就是给基类实现一个方法,那么其子类都可以使用)
type People struct {
Name string
Age int
Id int
}
func (p *People) showInfo() {
fmt.Println("info: ", p)
}
type Man struct {
People
Sex string
}
func (m *Man) showManInfo() {
fmt.Println("man info: ", m)
}
func main() {
man := Man{
People: People{
Name: "Listen",
Age: 1,
Id: 1,
},
Sex: "男",
}
man.showManInfo()
man.showInfo()
// 或者
man.People.showInfo()
}
type Q struct {
Name string
}
type W struct {
Q
Name string
}
func main() {
w := W{
Q: Q{
Name: "Q",
},
Name: "W",
}
fmt.Println(w.Name)
fmt.Println(w.Q.Name)
}
W
Q
解释:
(1)当我们直接通过w访问字段或方法时,其执行流程如下比如w.Name
(2编译器会先看w对应的类型有没有Name。如果有,则直接调用W类型的Name字段
(3)如果没有就去看W中嵌入的匿名结构体Q有没有声明Name字段,如果有就调用,如果没有继续查找…如果都找不到就报错.
访问字段和方法的时候都会有这个就近访问原则, 可以通过匿名结构体名来做区分
type Q struct {
Name string
}
type E struct {
Name string
}
type W struct {
Q
E
//Name string
}
func main() {
w := W{
Q: Q{
Name: "Q",
},
E: E{
Name: "E",
},
//Name: "W",
}
fmt.Println(w.Name)// error
fmt.Println(w.Q.Name) // right
}
如果一个struct 嵌套了一个有名结构体 ,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
当然也可以嵌入一个指针
type R struct {
Name string
}
type T struct {
*R
}
func main() {
t := T{
R: &R{
Name: "",
},
}
fmt.Println(t)
}
type Y struct {
int
string
}
func main() {
y := Y{
int: 0,
string: "",
}
fmt.Println(y.string)
fmt.Println(y.int)
}
严格来说,Go的interface类型是一组需要被其他数据类型实现的函数方法的集合。对于需要满足接口的数据类型,它需要实现接口所需的所有方法。
简单地说,接口是定义了一组需要被实现的函数方法的抽象类型,实现接口的数据类型可以视为接口的实例。
如果满足以上情况,我们说数据类型实现了这个接口。因此,一个接口包含两部分:一组接口方法和一个接口类型,这样就可以定义其它数据类型的行为。
使用接口的最大优势,就是在定义函数时使用接口作为参数,那么在调用函数时,任何实现该接口类型的变量都可以作为函数的参数被传递。接口可以作为抽象类型的能力,是其最实际的价值。
接口也是为了实现多态特性和高内聚低耦合
1)接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
2) Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有implement这样的关键字
比如一个复用逻辑的具体实现, 这个函数的参数我们可以使用接口来传递,只要传递来的这个参数符合某个接口,那么我们就可以多态的形式去执行这个接口里的方法
go里面的接口实现是基于方法的,至于其他毫不关心
package main
import (
"fmt"
"strconv"
"time"
)
type USB interface {
// 声明方法
Start(str string) string
Stop(str string) string
}
type Phone struct {
Id int
}
func (p *Phone) Start(str string) string {
// 将int转为string, 这种能避免乱码
return strconv.Itoa(p.Id) + " Phone start: " + str
}
func (p *Phone) Stop(str string) string {
return strconv.Itoa(p.Id) + " Phone stop: " + str
}
type Computer struct {
Id int
}
func (c *Computer) Start(str string) string {
return strconv.Itoa(c.Id) + " Computer start: " + str
}
func (c *Computer) Stop(str string) string {
return strconv.Itoa(c.Id) + " Computer stop: " + str
}
func Working(usb USB, desc string) {
fmt.Println(usb.Start(desc))
time.Sleep(time.Second)
fmt.Println(usb.Stop(desc))
}
func main() {
phone := Phone{
Id: 0,
}
computer := Computer{
Id: 1001,
}
Working(&phone, "手机") // 接口的本质是指针, 所以需要取地址
// 方式二
var usb USB
usb = &computer
Working(usb, "电脑")
}
0 Phone start: 手机
0 Phone stop: 手机
1001 Computer start: 电脑
1001 Computer stop: 电脑
type Stu struct {
Id int
}
type Stus []Stu
func (s Stus) Len() int { // 切片是引用传递,所以给其数据类型实现sort接口
return len(s) // 这里需要注意
}
func (s Stus) Less(i, j int) bool {
return s[i].Id > s[j].Id
}
func (s Stus) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func main() {
stus := Stus{}
stus = append(stus, Stu{
Id: 0,
}, Stu{
Id: 1,
}, Stu{
Id: 2,
}, Stu{
Id: 3,
}, Stu{
Id: 4,
}, Stu{
Id: 5,
}, Stu{
Id: 6,
}, Stu{
Id: 7,
})
sort.Sort(stus)
fmt.Println(stus)
}
// 方式二
var usb USB
usb = &computer
Working(usb, "电脑")
接口中所有的方法都没有方法体,即都是没有实现的方法。
在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
type USB interface {
// 声明方法
Start(str string) string
Stop(str string) string
}
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
type Integer int
func (i Integer) Start(str string) string {
return fmt.Sprint(i) + " Integer start: " + str
}
func (i Integer) Stop(str string) string {
return fmt.Sprint(i) + " Integer stop: " + str
}
func main() {
var i Integer
i = 10
fmt.Println(i.Stop("desc"))
}
一个接口可以实现多个接口
接口中不能有任何变量
一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
所以本质来说接口interface是一个指针(引用类型, 所以如果没有给一个接口就行初始化就使用,会报panic, 所以使用到接口参数的时候,最好先给接口进行if判断)
空接口interface没有任何方法,所以所有类型都实现了空接口
type T interface {
}
func main() {
var t T
t = Stu{
Id: 0,
}
fmt.Println(t)
t = Integer(1)
fmt.Println(t)
t = Computer{
Id: 0,
}
fmt.Println(t)
}
go可以通过匿名结构体字段实现继承,通过实现接口方法实现多态
所以继承是真的继承了代码, 而实现接口只是提供了一个自定义类型的规范
就比如我爸有钱,我可以直接继承他的钱,但是我爸会的技能,我还得通过实现接口才能表示我会这个技能
而多态就是基于接口实现,就和刚刚那个例子,虽然我和我弟都实现了那个接口学会了技能,但是我们俩个具体的做事风格还是不一样,企业招人就找会做的,但是做的风格又不一样,所以造成多态
go里面基于接口有参数的多态
在前面的Usb接口案例,Usb usb,即可以接收手机变量,又可以接收相机变量和电脑变量,就体现了Usb接口多态参数
go里还有数组的多态
演示一个案例:给Usb数组中,存放Phone结构体和Computer结构体变量
func main() {
usbs := [3]USB{}
usbs[0] = &Computer{}
usbs[1] = &Phone{}
usbs[2] = &Computer{}
fmt.Println(usbs)
}
我们上述演示的都是将一个变量赋予给接口, 那我们如何将一个接口赋予一个变量呢?
实现方式就是断言
看一段代码
func main() {
var t interface{}
point := Point{
x: 0,
y: 0,
}
t = point
fmt.Println(t)
// 如何将t重新赋予给Point
var point1 Point
point1 = t.(Point) // 类型断言
fmt.Println(point1.x, point1.y)
}
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
注意: 类型断言前后必须是完全相同的类型,否则就会失败, 比如int32转换前和转换后都必须是int32, 如果不一样断言前的类型,我们可以加上一些校验
func main() {
var t interface{}
var num float32
num = 1.1
t = num
num2, ok := t.(float64)
if !ok {
fmt.Println("convert error")
} else {
fmt.Printf("t type: %T value: %v\n", num2, num2)
}
}
convert error
func main() {
var t interface{}
var num float64
num = 1.1
t = num
num2, ok := t.(float64)
if !ok {
fmt.Println("convert error")
} else {
fmt.Printf("t type: %T value: %v\n", num2, num2)
}
}
t type: float64 value: 1.1
func typeJudge(items ...interface{}) {
for index, v := range items {
switch v.(type) {
case int:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v.(int), v)
case float32:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v.(float32), v)
case string:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v.(string), v)
case Point:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v.(Point), v)
case *Point:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v.(*Point), v) // 注意这个也是可以的
case int64:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v.(int64), v)
default:
fmt.Printf("第%v个参数 类型是%T 值是%v\n", index, v, v)
}
}
}
func main() {
itmes := make([]interface{}, 0)
var num int = 1
itmes = append(itmes, num)
var num2 float32 = 1.1
itmes = append(itmes, num2)
itmes = append(itmes, "listen")
itmes = append(itmes, Point{
x: 0,
y: 0,
})
itmes = append(itmes, &Point{
x: 1,
y: 1,
})
var num3 int64 = 9
itmes = append(itmes, num3)
typeJudge(itmes...)
}
第0个参数 类型是int 值是1
第1个参数 类型是float32 值是1.1
第2个参数 类型是string 值是listen
第3个参数 类型是main.Point 值是{0 0}
第4个参数 类型是*main.Point 值是&{1 1}
第5个参数 类型是int64 值是9
都是简单API, 大家看举例就行
func main() {
// 判断两个utf-8编码字符串(将unicode大写、小写、标题三种格式字符视为相同)是否相同。
fmt.Println(strings.EqualFold("ABC", "abc")) // ture
// 判断s是否有前缀字符串prefix。
fmt.Println(strings.HasPrefix("ABC", "AB")) // true
// 判断字符串s是否包含子串substr。
fmt.Println(strings.Contains("ABC", "B")) // true
// 返回将所有字母都转为对应的小写版本的拷贝。
fmt.Println(strings.ToLower("ABC")) // "abc"
// 返回将所有字母都转为对应的大写版本的拷贝。
fmt.Println(strings.ToUpper("abc")) // "ABC"
// 返回count个s串联的字符串。
fmt.Println(strings.Repeat("ABC", 2)) // "ABCABC"
// 返回将s前后端所有cutset包含的utf-8码值都去掉的字符串。
fmt.Println(strings.Trim(" ABC ", " ")) // "ABC"
// 返回将s前后端所有空白(unicode.IsSpace指定)都去掉的字符串。
fmt.Println(strings.TrimSpace("ABC ")) // "ABC"
// 用去掉s中出现的sep的方式进行分割,会分割到结尾,并返回生成的所有片段组成的切片(每一个sep都会进行一次切割,即使两个sep相邻,也会进行两次切割)。如果sep为空字符,Split会将s切分成每一个unicode码值一个字符串。
split := strings.Split("a b c d e", " ")
fmt.Println(split) // [a b c d e]
// 返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。
parseBool, err := strconv.ParseBool("F")
fmt.Println(parseBool, err)
// 返回字符串表示的整数值,接受正负号。
//base指定进制(2到36),如果base为0,则会从字符串前置判断,"0x"是16进制,"0"是8进制,否则是10进制;
//bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。
parseInt, err := strconv.ParseInt("1212", 10, 0)
fmt.Println(parseInt, err)
// 根据b的值返回"true"或"false"。
fmt.Println(strconv.FormatBool(true))
// 返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母'a'到'z'表示大于10的数字。
fmt.Println(strconv.FormatInt(111, 10))
// Atoi是ParseInt(s, 10, 0)的简写。
fmt.Println(strconv.Atoi("121"))
// Itoa是FormatInt(i, 10) 的简写。
fmt.Println(strconv.Itoa(1212))
}
os包提供了操作系统函数的不依赖平台的接口。设计为Unix风格的,虽然错误处理是go风格的;失败的调用会返回错误值而非错误码。
通常错误值里包含更多信息。
例如,如果某个使用一个文件名的调用(如Open、Stat)失败了,打印错误时会包含该文件名,错误类型将为*PathError,其内部可以解包获得更多信息。
type PathError struct {
Op string
Path string
Err error
}
有打开就必须讲关闭, 如果没有close这个文件资源的连接就会一直有, 所以会有内存泄漏和资源浪费
其实文件的本质就是数据加一些文件特有标识符, 文件在程序中是以流的形式来操作的。
流:
数据在数据源(文件)和程序(内存)之间经历的路径
输入流:数据从数据源(文件)到程序(内存)的路径
输出流:数据从程序(内存)到数据源(文件)的路径
os.File封装所有文件相关操作,File是一个结构体
func main() {
// 打开文件
file, err := os.Open("D:\\test\\study.txt")
if err != nil {
log.Fatal("open file error")
return
}
// 关闭
defer file.Close()
fmt.Println("file: ", file) // file: &{0xc0000d0780} 所以返回的file是一个指针
}
好处:不是一次性的把整个文件都读取进来,也不是一次性只读1个字节, 而是适配我们的内存进行读一部分, 处理一部分
如前所述,实现io.Reader接口需要实现Read()方法,io.writer()接口需要实现 write()方法。这两个接口在Go中只要涉及数据输入和输出会非常常见
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
缓冲文件输入和输出发生在读取或写入数据之前有一个临时存储数据的缓冲区。
因此,你可以一次读取多个字节,而不是逐字节地读取一个文件。你将它放在一个缓冲区中,等待调用者以所需的方式读取它。非缓冲文件输入和输出发生在实际读取或写入数据之前没有临时存储数据的缓冲区时。
你可能会问的下一个问题是,如何决定何时使用缓冲文件输入和输出,以及何时使用非缓冲文件输入和输出。
在处理关键数据时,非缓冲文件输入和输出通常是更好的选择,因为缓冲读取可能导致过期的数据,而缓冲写入可能在计算机电源中断时导致数据丢失。然而,大多数时候,这个问题没有明确的答案。这意味着你可以使用任何使任务更容易实现的工具。
顾名思义,bufio包是关于缓冲输入输出的。
所以bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。
具体API可以去官网查看
func main() {
// 打开文件
file, err := os.Open("D:\\test\\study.txt")
if err != nil {
log.Fatal("open file error")
return
}
// 关闭
defer file.Close()
// 创建一个带缓冲区的Reader
reader := bufio.NewReader(file)
// 读取内容
for {
// ReadString读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的字符串。
//如果ReadString方法在读取到delim之前遇到了错误
//它会返回在错误之前读取的数据以及该错误(一般是io.EOF)
readString, err := reader.ReadString('\n')
fmt.Println("error", err)
if err == io.EOF {
fmt.Println(readString)
break
}
fmt.Print(readString)
}
fmt.Println("文件读取结束")
}
基本介绍
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
说明: os.OpenFile是一个更一般性的文件打开函数,它会使用指定的选项(如
O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/o。如果出错,错误底层类型是*PathError。
const (
O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
O_RDWR int = syscall.O_RDWR // 读写模式打开文件
O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件
O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
)
const (
// 单字符是被String方法用于格式化的属性缩写。
ModeDir FileMode = 1 << (32 - 1 - iota) // d: 目录
ModeAppend // a: 只能写入,且只能写入到末尾
ModeExclusive // l: 用于执行
ModeTemporary // T: 临时文件(非备份文件)
ModeSymlink // L: 符号链接(不是快捷方式文件)
ModeDevice // D: 设备
ModeNamedPipe // p: 命名管道(FIFO)
ModeSocket // S: Unix域socket
ModeSetuid // u: 表示文件具有其创建者用户id权限
ModeSetgid // g: 表示文件具有其创建者组id的权限
ModeCharDevice // c: 字符设备,需已设置ModeDevice
ModeSticky // t: 只有root/创建者能删除/移动文件
// 覆盖所有类型位(用于通过&获取类型位),对普通文件,所有这些位都不应被设置
ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice
ModePerm FileMode = 0777 // 覆盖所有Unix权限位(用于通过&获取类型位)
)
这些被定义的位是FileMode最重要的位。另外9个不重要的位为标准Unix rwxrwxrwx权限(任何人都可读、写、运行)。这些(重要)位的值应被视为公共API的一部分,可能会用于线路协议或硬盘标识:它们不能被修改,但可以添加新的位。
r -> 4
w -> 2
x -> 1
func main() {
file, err := os.OpenFile("D:\\test\\study1.txt", os.O_WRONLY | os.O_CREATE, 0666)
if err != nil {
log.Fatal("open file error")
}
defer file.Close()
writer := bufio.NewWriter(file)
data := "hello"
for i := 0; i < 5; i++ {
_, _ = writer.WriteString(fmt.Sprintf("%v\n", data))
}
// 必须进行
_ := writer.Flush()
fmt.Println("写入成功")
}
func main() {
file, err := os.OpenFile("D:\\test\\study1.txt", os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0666)
if err != nil {
log.Fatal("open file error")
}
defer file.Close()
writer := bufio.NewWriter(file)
data := "byeBye"
for i := 0; i < 5; i++ {
_, _ = writer.WriteString(fmt.Sprintf("%v\r\n", data)) //这里使用\n是为了换行, 也支持/r/n进行换行
}
// 必须进行
_ = writer.Flush()
fmt.Println("写入成功")
}
func main() {
file, err := os.OpenFile("D:\\test\\study1.txt", os.O_WRONLY | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatal("open file error")
}
defer file.Close()
writer := bufio.NewWriter(file)
data := "Listen"
for i := 0; i < 5; i++ {
_, _ = writer.WriteString(fmt.Sprintf("%v\r\n", data)) //这里使用\n是为了换行, 也支持/r/n进行换行
}
// 必须进行
_ = writer.Flush()
fmt.Println("写入成功")
}
func main() {
file, err := os.OpenFile("D:\\test\\study1.txt", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatal("open file error")
}
defer file.Close()
// 先读出来
data := make([]string, 0)
reader := bufio.NewReader(file)
for {
readString, err := reader.ReadString('\n')
if err == io.EOF {
break
}
data = append(data, readString)
}
fmt.Println("读取数据成功: ", data)
// 写入数据
writer := bufio.NewWriter(file)
for i := 0; i < len(data); i++ {
_, _ = writer.WriteString(fmt.Sprintf("%v", data[i])) //这里使用\n是为了换行, 也支持/r/n进行换行
}
// 必须进行
_ = writer.Flush()
fmt.Println("写入成功")
}
func FileExist(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil { // 文件或者文件夹存在
return true, err
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
golang判断文件或文件夹是否存在的方法为使用os.Stat()函数返回的错误值进行判断:
1)如果返回的错误为nil,说明文件或文件夹存在
2)如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
3)如果返回的错误为其它类型,则不确定是否在存在
// 这种方式简单不严谨
func CopyFile(srcFileName, dstFileName string) bool {
data, err := ioutil.ReadFile(srcFileName)
if err != nil {
return false
}
fmt.Println(data)
err = ioutil.WriteFile(dstFileName, data, 0666)
return err == nil
}
// 合理的方式
func CopyFile2(srcFileName, dstFileName string) bool {
openFile, err := os.Open(srcFileName)
if err != nil {
return false
}
defer openFile.Close()
reader := bufio.NewReader(openFile)
dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
return false
}
defer dstFile.Close()
writer := bufio.NewWriter(dstFile)
_, err = io.Copy(writer, reader)
return err == nil
}
type Count struct {
CharCount int
NumCount int
SpaceCount int
OtherCount int
}
func count(data []string) Count {
c := Count{
CharCount: 0,
NumCount: 0,
SpaceCount: 0,
OtherCount: 0,
}
for _, val := range data {
for _, ch := range val {
switch {
case ch >= 'a' && ch <= 'z':
fallthrough
case ch >= 'A' && ch <= 'Z':
c.CharCount++
case ch >= '0' && ch <= '9':
c.NumCount++
case ch == ' ' || ch == '\t':
c.SpaceCount++
default:
c.OtherCount++
}
}
}
return c
}
func main() {
open, err := os.Open("D:\\test\\study.txt")
if err != nil {
return
}
reader := bufio.NewReader(open)
data := make([]string, 0)
for true {
readString, err := reader.ReadString('\n')
if err == io.EOF {
break
}
data = append(data, readString)
}
fmt.Println(count(data))
}
如果涉及到统计中文, 就必须先将字符串转为rune[]
比如:
cmd>main.exe -f c:/aaa.txt -p 200 -u root
这样的形式命令行,go设计者给我们提供了flag包,可以方便的解析命令行参数,而且参数顺序可以随意
请编写一段代码,可以获取命令行各个参数.
func main() {
fmt.Println("flags: ", os.Args)
// 解析数据
var f string
var p int
var u string
flag.StringVar(&f, "f", "", "文件路径,默认为空")
flag.IntVar(&p, "p", 0, "端口号,默认为0")
flag.StringVar(&u, "f", "root", "用户,默认为root")
flag.Parse() // 必须进行这步,进行数据解析
}
JSON易于机器解析和生成,并有效地提升网络传输效率,通常程序在网络传输时会先将数据(结构体、map等)序列化成json字符串,到接收方得到json字符串时,在反序列化恢复成原来的数据类型(结构体、map等).这种方式已然成为各个语言的标准。
https://www.json.cn
func main() {
// 演示将map进行序列化
data := make(map[string]interface{}, 0)
data["name"] = "listen"
data["age"] = 22
data["id"] = 329043988827384909
data["score"] = 98.5
marshal, err := json.Marshal(data)
if err != nil {
return
}
fmt.Println(string(marshal))
}
func main() {
jsonData := "{\"age\":22,\"id\":329043988827384909,\"name\":\"listen\",\"score\":98.5}"
var data map[string]interface{}
// 这里的map不需要make也能使用,因为在json.Unmarshal中已经进行了make
err := json.Unmarshal([]byte(jsonData), &data)
if err != nil {
return
}
fmt.Println(data)
}
打tag可以方便前端理解序列化后的数据
type Stu struct {
Name string `json:"name"`
Id int `json:"id"`
}
func main() {
stu := Stu{
Name: "Listen",
Id: 1001,
}
marshal, err := json.Marshal(stu)
if err != nil {
return
}
fmt.Println(string(marshal))
}
{“name”:“Listen”,“id”:1001}
Go语言中自带有一个轻量级的测试框架testing和自带的go test命 令来实现单元测试和性能测试,testing框 架和其他语言中的测试框架类似,可以基于这个框架写针对
相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,
可以解决如下问题:
1)确保每个函数是可运行,并且运行结果是正确的
2)确保写出来的代码性能是好的,
3)单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定
PS:
God单测的名字是有严格的规范,文件名是必须,因为是以_test作为文件结尾, 其原因就是为了让其作为扫描条件进行扫描, 函数名不是必须,但是函数名尽量友好以便阅读性
package study_testing
func add(a, b int) int {
return a + b
}
package study_testing
import (
"testing"
)
func TestAdd(t *testing.T) {
res := add(10, 20)
if res != 30 {
t.Fatal("test add 不符合预期")
}
t.Logf("test add 符合预期 res: %v\n", res)
}
D:\code\GO\20220102\study_testing> go test -v func.go func_test.go
=== RUN TestAdd
func_test.go:12: test add 符合预期 res: 30
— PASS: TestAdd (0.00s)
PASS
ok command-line-arguments 0.182s
看一下他的执行顺序
go test命令先会把XXX_test文件以文件名固定格式扫描到,对其进行编译后,在testing框架的main方法里执行TestAdd函数
1)测试用例文件名必须以。test.go 结尾。比如cal test.go , cal不是固定的。
2)测试用例函数必须以Test开头,一般来 说就是Test+被测试的函数名,比如TestAddUpper.
3) TestAddUpper(t tesing.T) 的形参类型必须是testing.T
4)一个测试用例文件中,可以有多个测试用例函数,比如TestAddUpper、 TestSub
5)运行测试用例指令
(1) cmd>go test [如果运行正确,无日志,错误时,会输出日志]
(2) cmd>go test-v [运 行正确或是错误,都输出日志]
6)当出现错误时, 可以使用t.Fatalf来格式化输出错误信息,并退出程序
7) t.Logf 方法可以输出相应的日志
8)测试用例函数,并没有放在main函数中,也执行了,这就是测试用例的方便之处
9) PASS表示测试用 例运行成功,FAIL 表示测试用例运行失败
进程和线程的简单说明
1)进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配的最小单位
2)线程是进程的一个执行实例,是程序执行的最小单元,也就是CPU调度的最小单位,它是比进程更小的能独立运行的基本单位。
3) 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行。
4)一个程序至少有一个进程,一个进程至少有一个线程
func main() {
go printHello()
for i := 0; i < 10; i++ {
fmt.Println("主线程: ", "hello ", i)
time.Sleep(time.Second)
}
fmt.Println("主线程: over")
}
func printHello() {
// 首先让协程休眠一秒
time.Sleep(time.Second)
for i := 0; i < 10; i++ {
fmt.Println("协程: ", "hello ", i)
time.Sleep(time.Second)
}
fmt.Println("协程: over")
}
上述代码协程的over不一定可以输出来,主要原因就是只要主线程退出,那么其开辟的协程就会退出
MPG模式基本介绍
1)M:操作系统的主线程(是物理线程)
2) P:协程执行需要的上下文
3)G:协程
MPG模式运行的状态1
1)当前程序有三个M(主线程),如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行,并且每个主线程都会有自己的上下文空间
2)M1,M2,M3都正在执行一个蓝色的G(协程),M1的协程等待队列有三个,M2的协程等待队列有3个,M3协程等待队列有2个
3)从上图可以看到: Go的协程是轻量级的线程,是用户态的,即使进行了协程调度也不会进行到内核态的转变,Go可以容易的起上万个协程。
4)其它程序c/java的多线程,往往进行调度上下文切换会进入内核态的,比较重量级,几千个线程可能耗光cpu
MPC运行的状态2
1)分成两个部分来看,先看左部分
2)原来的情况是M0主线程正在执行Go协程,另外有三个协程在队列等待
3)如果M0的Go协程阻塞,比如读取文件或者数据库等
4)这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写。
5)这样的MPG调度模式,可以既让Go执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。
6)等到Go不阻塞了,M0会被唤醒继续执行G0。
runtime这个包可以提供go运行时与环境的互操作,比如获取当前CPU的数量和设置最大的主线程数据
多线程模式下操作数据必然会造成数据的不确定性,降低系统的安全性,本质就是我们知道的本发写问题,一旦出现这类问题,极易造成panic
我们可以使用
go build -race
就可以看到程序有没有资源竞争问题
但是多线程编程自然也会大大的提高程序的执行和运算效率, 比如看下面需求
需求。现在要计算1-20 的各个数的阶乘,并且把各个数的阶乘放入到map 中。最后显示出来。要求使用goroutine完成
var (
retMap = make(map[int]uint64, 200)
)
func main() {
for i := 1; i <= 200; i++ {
go calculation(i)
}
time.Sleep(5 * time.Second)// 休眠5秒等待协程执行
fmt.Println(retMap)
}
func calculation(num int) {
var ret uint64 = 1 //防止数据放不下
for i := 2; i <= num; i++ {
ret = ret * uint64(i)
}
retMap[num] = ret
}
fatal error: concurrent map writes
会有异常爆出
var (
retMap = make(map[int]uint64, 200)
// 互斥锁
lock sync.Mutex
)
func main() {
for i := 1; i <= 20; i++ {
go calculation(i)
}
time.Sleep(5 * time.Second)// 休眠5秒等待协程执行
// 这里也需要加锁,防止出现并发读写问题
lock.Lock()
fmt.Println(retMap)
lock.Unlock()
}
func calculation(num int) {
var ret uint64 = 1 //防止数据放不下
for i := 2; i <= num; i++ {
ret = ret * uint64(i)
}
lock.Lock()
retMap[num] = ret
lock.Unlock()
}
加锁可以保证互斥性,防止出现并发读和并发写问题, 但是这个做法效率很低,需要阻塞等待写和读
var (
retMap = sync.Map{}
)
func main() {
for i := 1; i <= 20; i++ {
go calculation(i)
}
time.Sleep(2 * time.Second)
retMap.Range(func(key, value interface{}) bool {
fmt.Println("key: ", key, "value: ", value)
return true
})
}
func calculation(num int) {
var ret uint64 = 1 //防止数据放不下
for i := 2; i <= num; i++ {
ret = ret * uint64(i)
}
retMap.Store(num, ret)
}
使用这种方式,效率会有提升,但是我们不知道什么时候计算完,也就是在主线程那块,我们不知道休眠多久,以至于也会降低效率
为什么需要channel
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
1)主线程在等待所有goroutine全部完成的时间很难确定,我们之前代码设置10秒,仅仅是估算。
2)如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有
goroutine处于工作状态,这时也会随主线程的退出而销毁
3)通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
4)上面种种分析都在呼唤一个新的通讯机制-channel
var变量名chan数据类型举例:
var intChan chan int (intChan用于存放int数据)
var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
var perChan chan Person (用于存储Person类型)
var perChan2 chan Person (用于存储Person类型)
说明
1)channel是引用类型
2)channel 必须初始化才能写入数据 ,即make后才能使用
3)管道是有类型的.ntChan只能写入整数int
func main() {
// 初始化
var intCh chan int
intCh = make(chan int, 1) // 有缓存管道,可以缓存一个单位的数据
//写入数据
intCh <- 10
// 读出数据
ret := <- intCh
fmt.Println(ret)
}
func main() {
intCh := make(chan int, 3)
fmt.Printf("intCh的值: %v, intCh的类型: %T, intCh的地址: %p\n", intCh, intCh, &intCh)
// intCh的值: 0xc00007e000, intCh的类型: chan int, intCh的地址: 0xc000006028
}
由上我们可以知道,管道本身其实是存储了一个地址的指针,那个指针指向了一个另一个地址,增地址上存储了管道里的数据
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,否则会发生panic,但是仍然可以从该channel读取数据。
func main() {
intCh := make(chan int, 3)
fmt.Printf("intCh的值: %v, intCh的类型: %T, intCh的地址: %p\n", intCh, intCh, &intCh)
// intCh的值: 0xc00007e000, intCh的类型: chan int, intCh的地址: 0xc000006028
close(intCh)
}
channel支持for–range的方式进行遍历,强烈不推荐使用for循坏遍历,请注意两个细节
1)在遍历时,如果没有其他协程操作channel入口并且channel没有关闭,则回出现deadlock的错误
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
func main() {
intCh := make(chan int, 3)
intCh <- 1
intCh <- 2
intCh <- 3
close(intCh)
for ch := range intCh {
fmt.Println(ch)
}
}
intCh := make(chan int, 3)
intCh <- 1
intCh <- 2
intCh <- 3
close(intCh)
//for ch := range intCh {
// fmt.Println(ch)
//}
for true {
v, ok := <- intCh
if !ok {
break
}
fmt.Println(v)
}
需求。现在要计算1-20 的各个数的阶乘,并且把各个数的阶乘放入到map 中。最后显示出来。要求使用goroutine完成
为了最大化效率,我们可以使用协程加管道的方式实现
1)开启一个writeData协程,向管道intChan中写入50个整数.
2)开启一个readData协程,从管道intChan中读取writeData写入的数据。
3)注意: writeData和readDate操作的是同一个管道
4)主线程需要等待writeData和readDate协程都完成工作才能退出
5)所以使用一个exit管道控制主线程的退出
var dataMap = make(map[int]uint64, 20)
func main() {
intCh := make(chan int, 20)
exitCh := make(chan bool, 1)
go writeData(intCh)
go readData(intCh, exitCh)
// 下面这俩种写法都可以,也没有什么大区别
/* for true {
v, ok := <- exitCh
if !ok {
break
}
fmt.Println(v)
}*/
for ch := range exitCh {
if ch {
break
}
}
fmt.Println(dataMap)
}
func writeData(intCh chan int) {
for i := 1; i <= 20; i++ {
intCh <- i
}
// 如果管道不再需要放入数据就及时关闭
close(intCh)
}
func readData(intCh chan int, exitCh chan bool) {
for true {
num, ok := <- intCh
if !ok {
break
}
// 计算
var ret uint64 = 1 //防止数据放不下
for i := 2; i <= num; i++ {
ret = ret * uint64(i)
}
dataMap[num] = ret
}
// 计算完成后退出
exitCh <- true
close(exitCh)
}
一个协程不断的写数据,如果发现数据满了就会触发编译器处理,
如果编译器发现没有协程读这个管道的数据,就会立即爆出一个死锁的错误,如果编译器发现有协程去读这个管道,不管他快慢,都不会发生死锁deadlock,
读数据如果发现管道没有数据,同理,也会触发编译器的优化判断
需求:要求统计1-200000的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有goroutine和channel的知识后,就可以完成了
思路:
开启一个write协程往intchan管道写数据,开启4个read协程读数据然后计算,并且将结果写入primechan管道,最后由exitchan控制程序退出
func main() {
intChan := make(chan int, 50)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
//写数据
go writeData(intChan)
// 读数据
for i := 0; i < 4; i++ {
go readData(intChan, primeChan, exitChan)
}
// 控制退出
for i := 0; i < 4; i++ {
<- exitChan
}
close(primeChan)
close(exitChan)
for num := range primeChan {
fmt.Println(num)
}
}
func writeData(intChan chan int) {
for i := 0; i < 2000; i++ {
intChan <- i
}
close(intChan)
}
func readData(intChan chan int, primeChan chan int, exitChan chan bool) {
for num := range intChan {
flag := true
//判断num是不是素数
for i := 2; i < num;i++ {
if num % i == 0 {//说明该num不是素数
flag = false
break
}
}
if flag {
primeChan <- num
}
}
//当数据取完
exitChan <- true
}
func main() {
intChan := make(chan int, 2)
testIn(intChan)
testOut(intChan)
}
func testOut(outChan <-chan int) {
fmt.Println(<- outChan)
fmt.Println(<- outChan)
}
func testIn(inChan chan<- int) {
inChan <- 10
inChan <- 22
}
使用这种方式可以提高代码阅读性和,防止误操作,编译器也会对其进行优化,效率也会有些许提升
之前我们发现如果从一个管道读数据并且此时管道缓冲区为空,然后写数据的协程又很慢,就会造成读数据的协程阻塞,为了避免阻塞,select可以按顺序查看管道数据是否准备就绪,只要有一个管道有数据,就会退出select,如果都没有准备好,就会走default
func main() {
one := make(chan int, 1)
two := make(chan int, 1)
three := make(chan int, 1)
go testPut(one, 1)
go testPut(two, 2)
go testPut(three, 3)
for {
flag := false
select {
case v := <-one:
fmt.Println("one: ", v)
case v := <-two:
fmt.Println("two: ", v)
case v := <-three:
fmt.Println("three: ", v)
default:
fmt.Println("over")
close(one)
close(two)
close(three)
flag = true
}
if flag {
break
}
time.Sleep(time.Second)
}
}
func testPut(ch chan<- int, times int) {
for i := 1; i < times * 3; i++ {
num := i * times
ch <- num
}
//close(ch) // 使用elect的时候这里不能close, 不然select的对应channel的case处一直会返回零值
}
two: 2
one: 1
one: 2
two: 4
two: 6
three: 3
three: 6
three: 9
two: 8
three: 12
two: 10
three: 15
three: 18
three: 21
three: 24
over
Process finished with exit code 0
基本介绍
1)反射可以在运行时动态获取变量的各种信息,比如变量的类型,类别
2)如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段方法)
3)通过反射,可以修改变量的值,可以调用关联的方法。
4)使用反射,需要import (“reflect”)
reflect包实现了运行时反射,允许程序操作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。
func main() {
num := 10
// 获得valueOf
valueOf := reflect.ValueOf(num)
fmt.Println(valueOf.Kind()) // int
// 通过valueOf获得typeOf
i := valueOf.Interface()
typeOf := reflect.TypeOf(i)
fmt.Println(typeOf.Kind()) // int
// 原获取到num
num2 := i.(int)
fmt.Println(num2)
// 直接获取typeOf
of := reflect.TypeOf(num2)
fmt.Println(of.Kind()) // int
}
不知道接口调用哪个函数,根据传入参数在运行时确定调用的具体接口,这种需要对函数或方法反射.例如以下这种桥接模式
// 第一个参数funcPtr以接口的形式传入函数指针,
// 函数参数args以可变参数的形式传入,
// bridge函数中可以用反射来动态执行funcPtr函数
func bridge(functionPtr interface{}, args ...interface{}) {
}
tag
就是通过反射获得对应字段下的tag参数, 然后生成想要的json字符串//专门用于做反射
func test(b interface{}) {
//1.如何将Interface}转成reflect.value
rVal := reflect.ValueOf(b)
//2.如何将reflect.Value-> interface{}
ival :=rVal.Interface()
//3.如何将interface{}转成原来的变量类型,使用类型断言
v:= ival.(int)
fmt.Println(v)
}
kind是类别,type是类型,type是具体的类型,kind是属于某一个类别
Type是类型,Kind是类别,Type和Kind可能是相同的,也可能是不同的.比如: var num int = 10 num的Type是int , Kind也是int
比如: var stu Student stu的Type是包名.Student , Kind是struct
请编写一个案例,演示对(基本数据类型、interfacef}、reflect.Value)进行反射的基本操作
func main() {
// 通过反射计算和修改数据
num := 10
valueOf := reflect.ValueOf(num)
// 计算数据
i := valueOf.Int()
fmt.Println( i+ 10) // 20
// 修改原数据, 注意这里传递的是地址
valueOf = reflect.ValueOf(&num)
// elem用于获取指针指向的变量
valueOf.Elem().SetInt(20)
fmt.Println(num) // 20
}
注意这里使用valueOf.Int() 要确保之前的数据也是int才可以,不然会发生panic, 最稳妥的做法就是先转换成空接口interface{} 然后断言成具体的类型
必须清楚是什么类型,不然会报panic, 如果是结构体,得进行断言
通过反射的来修改变量,注意当使用Setxxx方法来设置, 需要通过对应的指针类型来完成,这样才能改变传入的变量的值,同时需要使用到reflect.Value.Elem()方法
.elem用于获取指针指向的变量
给你一个变量var v float64 = 1.2,请使用反射来得到它的reflect.Value,然后获取对应的Type,Kind和值, ,并将reflect.Value转换成interface,再将interface转换成float64, 最后修改为2.4, 输出v显示2.4
func main() {
var v float64 = 1.2
valueOf := reflect.ValueOf(v)
fmt.Println(valueOf.Kind())
fmt.Println(valueOf.Type())
i := valueOf.Interface()
f := i.(float64)
fmt.Println(f == v)
of := reflect.ValueOf(&v)
of.Elem().SetFloat(2.4)
fmt.Println(v)
}
float64
float64
true
2.4
主要的几个方法使用 :
method, 默认按方法名排序对应i值i从0并始, 方法排序按照函数名字排序,比较函数名字的的字母的ascll码
方法执行调用, in为传入的参数,返回值和参数都是切片, 而且是value类型的切片
获得结构体字段, 和获得方法类似
反射的struct tag 的核心代码:
tag := tye.Elem().Field(0).Tag.Get(" json"")
type Monster struct {
Name string `myJson:"monster_name"`
Age int `myJson:"monster_age"`
Score float32
Sex string
}
func (s Monster)Print() {
fmt.Println(" ---start----")
fmt.Println(s)
fmt.Println( "---end----")
}
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{}) reflect.Value {
valueOf := reflect.ValueOf(a)
kind := valueOf.Kind()
// 判断是否为结构体
if kind != reflect.Struct {
log.Fatal("not a struct")
}
return valueOf
}
func main() {
monster := Monster{
Name: "jack",
Age: 21,
Score: 100,
Sex: "man",
}
valueOf := testStruct(monster)
typeOf := reflect.TypeOf(monster)
// 获得结构体字段数目
numField := valueOf.NumField()
fmt.Println("Monster has ", numField, " fields")
// 获得所有结构体字段,并遍历
for i := 0; i < numField; i++ {
fieldValue := valueOf.Field(i)
fieldType := typeOf.Field(i)
fmt.Println("Field ", i, " type is ", fieldType)
fmt.Println("Field ", i, " value is ", fieldValue)
// 获得tag数据
get := typeOf.Field(i).Tag.Get("myJson")
if get != "" {
fmt.Println("Field ", i, " myJson is ", get)
}
}
fmt.Println("====================================")
// 获得方法数据, 如果方法是小写的,这里是统计不到的
numMethod := valueOf.NumMethod()
fmt.Println("Monster has ", numMethod, " methods")
// 遍历方法
for i := 0; i < numMethod; i++ {
method := typeOf.Method(i)
fmt.Println("Method ", i, " type is ", method)
}
fmt.Println("====================================")
// 执行第一个方法 Print, Print方法没有参数,所以传nil
valueOf.Method(1).Call(nil)
// 调用执行有参数的方法 GetSum
params := make([]reflect.Value, 0)
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(20))
retValues := valueOf.Method(0).Call(params) // 传入和传出的参数都是value切片
fmt.Println(retValues[0].Int())
}
Monster has 4 fields
Field 0 type is {Name string myJson:"monster_name" 0 [0] false}
Field 0 value is jack
Field 0 myJson is monster_name
Field 1 type is {Age int myJson:"monster_age" 16 [1] false}
Field 1 value is 21
Field 1 myJson is monster_age
Field 2 type is {Score float32 24 [2] false}
Field 2 value is 100
Field 3 type is {Sex string 32 [3] false}
Field 3 value is man
====================================
Monster has 3 methods
Method 0 type is {GetSum func(main.Monster, int, int) int <func(main.Monster, int, int) int Value> 0}
Method 1 type is {Print func(main.Monster) <func(main.Monster) Value> 1}
Method 2 type is {Set func(main.Monster, string, int, float32, string) <func(main.Monster, string, int, float32, string) Value> 2}
====================================
---start----
{jack 21 100 man}
---end----
30
type User struct {
Name string
Age int
Id int
}
func main() {
user := User{
Name: "",
Age: 0,
Id: 0,
}
// 获取*user的value
valueOf := reflect.ValueOf(&user)
// 获取 *user的一个新的指针
elem := valueOf.Elem()
elem.FieldByName("Name").SetString("listen")
elem.FieldByName("Age").SetInt(20)
elem.FieldByName("Id").SetInt(1001)
fmt.Println(user)
}
type User struct {
Name string
Age int
Id int
}
func main() {
user := User{
Name: "",
Age: 0,
Id: 0,
}
typeOf := reflect.TypeOf(user)
// 这里返回的是一个newUser 的指针
newUser := reflect.New(typeOf)
// 获取newUser 的一个指针
elem := newUser.Elem()
elem.FieldByName("Name").SetString("listen")
elem.FieldByName("Age").SetInt(20)
elem.FieldByName("Id").SetInt(1001)
u := newUser.Interface().(*User)
fmt.Println(u)
}
网络编程有三种:
1)TCP socket编程,是网络编程的主流。之所以叫Tcp socket编程,是因为底层是基于Tcp/ip协议的.比如:QQ聊天
2) b/s结构的http编程,我们使用浏览器去访问服务器时,使用的就是http协议,而http底层依旧是用tcp socket实现的。比如:京东商城【这属于go web开发范畴】
3)RPC服务编程,这种方式属于微服务区域的远程方法调用,但是在Go中有集成
ip地址
概述:每个inter net业的主机和路由器都有一个ip地址,它包括网络号和主机号,ip地址有ipv4(32位)或者ipv6(128位).可以通过ipconfig 来查看
端口号
我们这里所指的端口不是指物理意义上的端口,而是特指TCP/IP协议中的端口,是逻辑意义上的端口。
如果把IP地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个IP地址的端口可以有65536(即:256×256)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从o到65535 (256×256-1)
端口(port)-分类
0号是保留端口.
1-1024是固定端口
又叫有名端口,即被某些程序固定使用,一般程序员不使用 22:SSH远程登录协议 23: telnet使用 21:ftp使用 25: smtp服务使用 80: iis使用 7: echo服务
1025-65535是动态端口
这些端口,程序员可以使用.
>端口(port)-使用注意
1)在计算机(尤其是做服务器)要尽可能的少开端口
2)一个端口只能被一个程序监听
3)如果使用netstat -an可以查看本机有哪些端口在监听
4)可以使用netstat -anb来查看监听端口的pid,在结合任务管理器关闭不安全的端口.
tcp socket编程会分为客户端和服务器端
服务端的处理流程
1)监听端口
2)接收客户端的tcp链接,建立客户端和服务器端的链接.
3)创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
客户端的处理流程
1)建立与服务端的链接
2)发送请求数据,接收服务器端返回的结果数据
3)关闭链接
func main() {
fmt.Println("服务器开始监听...")
// net.Listen("tcp","o.0.0.0:8888")
// 1. tcp表示使用网络协议是tcp
// 2. 0.0.0.0.0:8888 表示在本地监听8888端口
listen, err := net.Listen( "tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen error")
return
}
defer listen.Close() // 必须关闭
// 循坏等待客户端来连接
for true {
fmt.Println("等待客户端连接...")
accept, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
} else {
//获取到连接进行逻辑处理
fmt.Println("accept success: ", accept)
//创建一个新的切片,用户读取客户端发送来的数据
buf := make([ ]byte, 1024)
//1.等待客户端通过conn发送信息
//2.如果客户端没有write[发送],那么就会就阻塞在这里
n , err := accept.Read(buf) //从连接中读取
if err != nil {
fmt.Println("服务器的Read err=", err)
}
//3.显示客户端发送的内容到服务器的终端
fmt.Println("接受到的数据为: ", string(buf[:n]))
}
}
}
func main() {
fmt.Println("客户端建立...")
dial, err := net.Dial("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("客户端请求连接失败", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出, 在这里获得输入的数据
reader := bufio.NewReader(os.Stdin) //os.stdin代表标准输入[终端]
//从终端读取一行用户输入,并准备发送给服务器
line, err := reader.ReadString('\n') // 以换行为输入的结尾
if err != nil {
fmt.Println( "ReadString err=", err)
}
//再将line发送给服务器
n, err := dial.Write([]byte(line))
if err != nil {
fmt.Println( "conn write err=",err)
}
fmt.Printf("客户端发送了%d字节的数据,并退出",n)
}
远程调用(RPC)是一种使用TCP/IP的进程间通信的客户端-服务器机制。通过这种机制实现远程的方法调用,以便于程序的解耦
通过远程调用方法, 实际的识别该调用哪个,是通过服务端和客户端相同的方法 参数 返回值标识的,所以一定要有一个服务端和客户端都一样的接口, 客户端只需要得到接口的方法标识即可, 服务端需要实现接口的方法
package sharedRPC
// 共享结构体
type MyFloats struct {
A1, A2 float64
}
// 共享接口
type MyInterface interface {
Multiply(arguments *MyFloats, reply *float64) error
Power(arguments *MyFloats, reply *float64) error
}
/*
sharedRPC包定义了一个名为MyInterface的接口和一个名为MyFloats 的结构,
客户端和服务器都将会使用到。
只有RPC服务器需要实现这个接口。
*/
package main
import (
"./sharedRPC"
"fmt"
"net/rpc"
)
func main() {
//注意,尽管RPC服务器使用TCP,但使用rpc.Dial()函数代替net.Dial()连接RPC服务器。
c, err := rpc.Dial("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
args := sharedRPC.MyFloats{
A1: 16,
A2: -0.5,
}
//RPC客户端和RPC服务器之间通过call()函数交换函数名,参数和函数返回结果,
//而RPC客户端对函数的具体实现一无所知。
var reply float64
err = c.Call("MyInterface.Multiply", args, &reply)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Reply (Multiply): %f\n", reply)
err = c.Call("MyInterface.Power", args, &reply)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Reply (Power): %f\n", reply)
}
package main
import (
"./sharedRPC"
"fmt"
"math"
"net"
"net/rpc"
)
// 服务端需要实现MyInterface 接口
type MyInterface struct {}
func (t *MyInterface) Multiply(arguments *sharedRPC.MyFloats,
reply *float64) error {
*reply = arguments.A1 * arguments.A2
return nil
}
func (t *MyInterface) Power(arguments *sharedRPC.MyFloats,
reply *float64) error {
*reply = Power(arguments.A1, arguments.A2)
return nil
}
func Power(x, y float64) float64 {
return math.Pow(x, y)
}
func main() {
myInterface := new(MyInterface)
/*
rpc.Register()函数的调用使这个程序成为RPC服务器。
但是,由于RPC服务器使用TCP协议,
它仍需要调用net.ResolveTCPAddr()和net.ListenTCP()。
*/
err := rpc.Register(myInterface)
if err != nil {
fmt.Println("register error", err)
return
}
t, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
l, err := net.ListenTCP("tcp4", t)
if err != nil {
fmt.Println(err)
return
}
for {
c, err := l.Accept()
if err != nil {
continue
}
/*
RemoteAddr()函数返回接入的RPC客户端IP地址和端口。
rpc.ServerConn()函数为RPC客户端提供服务。
*/
fmt.Printf("%s\n", c.RemoteAddr())
rpc.ServeConn(c)
}
}
Reply (Multiply): -8.000000
Reply (Power): 0.250000
调用成功