Go,冲了

Go!

文章目录

  • Go!
    • 变量定义
      • 变量
      • 常量
    • 基本数据类型
      • int
      • float
      • bool
      • string
      • byte
      • rune
      • 修改字符串
      • strconv
    • 流程控制
      • for
      • for range
      • switch
      • goto
    • 数组
    • 切片
      • 基于数组定义切片
      • 基于切片再切片
      • 关于切片的长度和容量
      • 本质
      • make()创建一个切片
      • append()
      • copy()
      • 从切片中删除元素
    • sort包
    • 复合数据类型
      • map
    • 函数
      • 函数定义
      • 函数的可变参数
      • 函数类型与变量
      • 匿名函数和闭包
      • defer
      • panic + recover
    • time包及日期函数
    • 指针(童年阴影)
    • 结构体(害怕)
      • 结构体的定义
      • 结构体实例化
      • 结构体方法和接收者
      • 结构体嵌套和继承
      • 结构体序列化与反序列化Json
      • 嵌套结构体的序列化与反序列化
    • go mod以及go包
      • go包管理工具go mod
      • init()函数
      • go中使用第三方包
        • 安装这个包
        • 引入这个包
    • 接口
      • 接口的定义
      • 空接口
      • 类型断言
      • 结构体值接收者和指针接收者实现接口的区别
      • 接口方法有返回值的情况
      • 一个结构体实现多个接口,接口嵌套
      • 空接口和类型断言的使用细节
    • goroutine协程
      • 为什么要使用goroutine
      • Golang中的协程(goroutine) 以及主线程
      • Goroutine的使用以及sync.WaitGroup
        • 使用sync.WaitGroup
      • 设置golang并行运行时候占用的cpu数量
      • 一个需求
    • channel管道
      • channel类型
      • 创建channel
      • channel操作
      • goroutine结合channel
      • gorutine结合channel实现统计素数
      • 单向管道
      • select多路复用
      • goruntine recover解决协程中出现的panic
    • go并发安全和锁
      • 互斥锁
      • 读写锁
    • 反射
      • 反射的基本介绍
      • go可以实现的功能
      • reflect.TypeOf()获取任意值的类型对象
      • reflect.ValueOf()
      • 结构体反射
      • 不要乱用反射!
    • 文件 目录操作
      • 读取文件
      • 写入文件
      • 目录操作

变量定义

变量

var

str := "aaa"  //自动推断字符串类型
num := 10	//int类型

var(
	n1 = 10
    n2 = 20
    n3 = 30
)

常量

const

const NAME := "kk"
const(
	N1 = 0
    N2 = 1
    N3 = 3
)

//iota 计数器
const (
	a = iota	//0
	b			//1
	c			//2
	_			//跳过
	d			//4
)

const (
	a = iota	//0
	b = 100		//100
	c = iota	//2
	d			//3
)

const (
	a, b = iota + 1, iota + 2 //1,2
	c, d                      //2,3
	e, f                      //3,4
)

基本数据类型

int

Go,冲了_第1张图片

强制转换

var a1 int32 = 10
var a2 int64 = 20
fmt.Println(int64(a1) + a2)    //30
num := 12
fmt.Printf("num=%v\n", num) //%v 原样输出
fmt.Printf("num=%d\n", num) //%d 表示10进制输出
fmt.Printf("num=%b\n", num) //%b表示二进制输出
fmt.Printf("num=%o\n", num) //%o八进制输出
fmt.Printf("num=%x\n", num) //%x表示16进制输出

float

Go,冲了_第2张图片

使用科学计数法表示浮点数据

var f2 float32 = 3.14e2 //表示f2等于3.14*10的2次方
fmt.Printf ("%v--%T",f2,f2)
//314--float32

bool

var flag = true

string

var str = "string"

常用方法

引入strings包

方法 介绍
len(str) 长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.join(a[] string, sep string) join操作

byte

字节嘛

rune

这个和char差不多

用增强型for循环遍历就不会把中文搞开了

s := "你好golang"
for _,v := range s{
    fmt.Printf("%v(%c)",v,v)
}

//20320(你) 22909(好) 32( ) 103(g) 111(0) 108(1) 97(a) 110(n) 103(g)

修改字符串

Go,冲了_第3张图片

strconv

把其他类型转换成string类型,也可以反向转换

i := 20
str1 := strconv.FormatInt(int64(i),10)		//两个参数,要转换的变量(要求int64),进制

流程控制

for

for i := 0;i<10;i++{
    fmt.Print("*")
}

for range

增强型for循环

for k,v := range str{
    fmt.Printf("%v---%c",k,v)	
}
//str是个字符串数组,k为字符的下标,v为字符
//不想打印下标也可以用_省略

for _,v := range str{
    fmt.Printf("%c",v)
}

switch

var n = 8
switch n {
	case 1,3,5,7,9 :
    	fmt.Println("奇数")
    	break
    case 2,4,6,8,10:
    	fmt.Println("偶数")
    	break
}

注意:

  • go中的switch case分支语句可以有多个值
  • 每一个case可以不用写break,不会穿透,不过还是写上比较好
  • 使用fallthrough可以进行手动穿透,只能穿透一层

goto

var n = 30
if n > 24{
    fmt.Println("成年人")
    goto label
}
fmt.Println("aaa")
fmt.Println("bbb")

label:
fmt.Println("ccc")
fmt.Println("ddd")

说明,当走到goto之后,程序会直接跳到对应的label,label中间的就不会被执行了

数组

//1 初始化0值
var nums = [3]int
var strs = [4]string

//2 初始化
var arr = [4]int{0,1,2,3}

//3 自行推断数组长度
var arr1 = [...]string{"php","java","golang"}

fmt.Println(arr)  //[0,1,2,3]

**值类型:**改变变量副本的值,不会改变变量本身的值

**引用类型:**改变变量副本的值,会改变变量本身的值

切片

就是把声明数组时把长度去掉

var name []int	//声明了切片以后,切片的默认值就是nil

基于数组定义切片

a := [5]int{1,2,3,4,5}	//定义了一个长度为5的数组
b := a[:]	//获取数组里面的所有值
c := a[1:4]	//指定获取数组中的内容组成切片,左闭右开	1,2,3
d := a[2:]	//3,4,5
e :=a[:3]	//1,2,3

基于切片再切片

a := []string{"北京","上海","广州","深圳","成都","重庆"}
b := a[1:]	//差不多

关于切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

  • 长度:切片的长度就是它所包含的元素个数
  • 容量:切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
s := []int{1,2,3,4,5,6}
fmt.Printf("长度%d 容量%d\n", len(s)cap(s)) //长度6容量6

a := s[2:]
fmt.Printf("长度%d 容量%d\n", len(a)cap(a)) //长度4容量4

b := s[1:3]
fmt.Printf("长度%d 容量%d\n", len(b)cap(b)) //长度2容量5	底层数组末尾,所以是5

本质

本质就是对底层数组的封装,包含三个信息,底层数组的指针,切片长度len,和切片容量cap

make()创建一个切片

var sliceA = make([]int,4,8)	//make(切片类型,len,cap)	有默认值,打印一下是[0 0 0 0]

append()

扩容

//golang中没法通过下标的方式给切片扩容
//golang中给切片扩容的话要用到append()方法
var sliceA []int
sliceA = append( sliceA,12)
sliceA = append(sliceA,24)

//一次传入多个值
sliceA = append(sliceA,1,2,3,4)

合并

sliceA := []string{"php", "java"}
sliceB := []string{"nodejs", "go"}
sliceA = append(sliceA, sliceB...)	//切片合并	[php java nodejs go]

切片的扩容策略

有三种扩容策略,上源码

newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

copy()

sliceA := []int{1, 2, 3, 45}
sliceB := make([]int, 4, 4)
copy(sliceB, sliceA)		//直接进行赋值是浅拷贝,引用传递,使用copy就是深拷贝,值传递了

sliceB[0] = 111				//此时对切片进行改变并不会影响原来切片的值
fmt.Print1n(sliceA)
fmt.Println(sliceB )

从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素

a := []int{30,31 ,32 ,33 ,34,35,36,37}	//想要删除索引为2的元素
a = append(a[1:2],a[3:]...)			//使用append进行操作,appen合并的时候最后一个元素要加...

sort包

升序

sort.Ints()		//整型排序
sort.Float64s()	//浮点型排序
sort.Strings()	//字符串排序

降序

sort.Sort(sort.Reverse(sort.IntSlice(intList)))		//整型降序
sort.Sort(sort.Reverse(sort.Float64Slice(floatList)))	//浮点型降序
sort.Sort(sort.Reverse(sort.StringSlice(floatList)))	//字符串降序

复合数据类型

map

k-v

使用make创建一个map

make(map[KeyType]ValueType,[cap])

cap表示map的初始容量,该参数不是必须的

var userinfo = make(map[string]string)
userinfo["name"] = "张三"

在声明map的时候直接填充数据

var userinfo = map[string]string{
    "username":	"张三",
    "age": "20",
    "gender":"男",
}

循环遍历map

for k,v := range userinfo{
    fmt.Printf("%v:%v",k,v)
}

map的crud

//创建,修改map数据
userinfo := make(map[string]string)
userinfo["name"] = "张三"
userinfo["age"]	= "20"
fmt.Println(userinfo)	//map[name:张三]

//获取map的数据
username := userinfo["name"]
fmt.Println(username)	//张三

//查看map中是否包含key
v,ok := userinfo["name"]
fmt.Println(v,ok)		//张三 true

//v,ok := userinfo["xxx"]
//fmt.Println(v,ok)		//(空) false

//删除map中的kv
delete(userinfo,"name")
fmt.Println(userinfo)	//map[age:20]

创建元素为map类型的切片

//我们想在切片里面放一系列用户的信息,这时候我们就可以定义一个元素为map类型的切片
userinfo := make([]map[string]string,3,3)
fmt.Println(userinfo)	//map[]		map不初始化的默认值nil
if userinfo[0] == nil {
    userinfo[0] = make(map[string]string)
    userinfo[0]["name"] = "张三"
    userinfo[0]["age"] = "20"
    userinfo[0]["height"] = "180cm"
    userinfo[0]["gender"] = "男"
}
if userinfo[1] == nil {
    userinfo[1] = make(map[string]string)
    userinfo[1]["name"] = "李四"
    userinfo[1]["age"] = "21"
    userinfo[1]["height"] = "179cm"
    userinfo[1]["gender"] = "女"
}
for _,v := range userinfo{
    fmt.Println(v)
}
/**
    [map[] map[] map[]]
    map[age:20 gender:男 height:180cm name:张三]
    map[age:21 gender:女 height:179cm name:李四]
    map[]
*/

将切片作为map的value

//如果我们想在map对象中存放一系列的属性的时候,我们就可以把map类型的值定义成切片
userinfo := make(map[string][]string)
userinfo["hobby"] = []string{
    "吃饭","睡觉","rip",
}
userinfo["work"] = []string{
    "需求","设计","实现",
}
fmt.Println(userinfo)

/**
	map[hobby:[吃饭 睡觉 rip] work:[需求 设计 实现]]
*/

map也是引用类型

小练习

//写一个程序,统计一个字符串中每个单词出现的次数。比如: "how do you do"中how=1 do=2 you=1
str := "how do you do"
strs := strings.Split(str," ")
count := make(map[string]int)
for _,v := range strs{
    count[v]++
}
fmt.Println(count)

函数

函数定义

func 函数名(参数)(返回值){
    函数体
}

两数相加

func twoSum(x int, y int) int {
	return x + y
}
func main() {
	fmt.Println(twoSum(11,2))
}

如果入参的类型是一样的,可以省略,直接写最后

func twoSum(x, y int) int {
	return x + y
}

函数的可变参数

理解为一个切片

func change(x ...int) {
	fmt.Printf("%v----%T",x,x)
}

func main(){
    change(1,2,3,4,5,9,2)	//[1 2 3 4 5 9 2]----[]int		
}
//代表参数中,第一个参数赋给x,剩下的都赋给y
func change(x int, y ...int) {
	fmt.Printf(x,y)
    sum := X
	for v := range y{
		sum += v
	}
	return sum
}

多个返回值

func calc(x, y int) (int,int) {
	sum := x+y
	sub := x-y
	return sum, sub
}
//给返回值命名,这样就不用在函数体中声明返回值了
func calc(x, y int) (sum, sub int) {
	sum = x+y
	sub = x-y
	return sum, sub
}

封装一个降序排序函数

func sortIntDesc(since []int)  {
	sort.Sort(sort.Reverse(sort.IntSlice(since)))
}

函数类型与变量

定义函数类型

type calc func(int, int) int //定义一个为calc的函数类型

func add(x, y int) int {
	return x + y
}
func sub(x, y int) int {
	return x - y
}

func calculation(x, y int,op calc) int {
	return op(x,y)
}

func main() {
	var c calc
	c = add
	fmt.Println(calculation(2,3,c))	//5
}
/**
也可以传一个匿名函数
fmt.Println(calculation(2,3, func(x int, y int) int {
		return x * y
	}))
*/

让函数返回函数

type calc func(int, int) int //定义一个为calc的函数类型

func add(x, y int) int {
	return x + y
}
func sub(x, y int) int {
	return x - y
}
func mul(x, y int) int {
	return x * y
}
func div(x, y int) int {
	return x / y
}

func myfun(o string) calc {
	switch o {
	case "+":
		return add
	case "-":
		return sub
	case "*":
		return mul
	case "/":
		return div
	default:
		return nil
	}
}

func main() {
	c := myfun("*")
	fmt.Println(c(2,3))	//6
}

匿名函数和闭包

匿名函数没有名字

func(参数)(返回值){
    函数体
}
func main(){
    //匿名函数
    func() {
        fmt.Println("test..")
    }()	//在这里我们加一个() 表示执行这个匿名函数本身	匿名自执行函数
    
    //匿名自执行函数接收参数
	func(x, y int) {
		fmt.Println(x +y)
	}(10, 20)

}

闭包

全局变量特点:

  • 常驻内存
  • 可能污染全局

局部变量的特点:

  • 不常驻内存
  • 不污染全局

闭包

  • 可以让一个变量常驻内存
  • 可以让一个变量不污染全局

闭包是指有权访问另一个函数作用域中的变量的函数

创建闭包的常见的方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量

注意:由于闭包里作用域返回的局部变量资源不会被立刻销毁回收,所以可能会占用更多的内存。过度使用闭包会导致性能下降,建议在非常有必要的时候才使用闭包。

func adder() func() int {
	var i = 10
	return func() int {
		return i + 1
	}
}

func adder1() func(y int) int {
	var i = 10
	return func(y int) int {
		i += y
		return i
	}
}

func main() {
	var fn = adder()	//执行方法
	fmt.Println(fn())	//11
	fmt.Println(fn())	//11
	fmt.Println(fn())	//11

	var fn1 = adder1()	//执行方法
	fmt.Println(fn1(10))	//20
	fmt.Println(fn1(10))	//30
	fmt.Println(fn1(10))	//40
}

defer

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

defer

把语句像栈一样执行,先进后出

func main() {
	fmt.Println("开始")
	fmt.Println(1)
	fmt.Println(2)
	fmt.Println(3)
	fmt.Println("结束")
}
/**
	开始	1 2 3 结束
*/
func main() {
	fmt.Println("开始")
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
	fmt.Println("结束")
}
/**
	开始	3 2 1 结束
*/

defer在命名返回值和匿名返回函数中表现不一样

func f1() {
	fmt.Println("开始")
	defer func() {
		fmt.Println("aaaa")
    }()		//注意:此处必须是匿名自执行方法
	fmt.Println("结束")
}
func main() {
	f1()
}
/**
	开始
	结束
	aaaa
*/
//匿名返回值		执行结果是0
func f2() int {
	var a int
	defer func() {
		a++
	}()
	return a
}
func main() {
    fmt.Println(f2())		//0
}
//命名返回值		执行结果是1
func f2() (a int) {
	defer func() {
		a++
	}()
	return a
}
func main() {
    fmt.Println(f3())		//1
}

defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET 指令执行前。具体如下图所示:

Go,冲了_第4张图片

panic + recover

Go语言中目前是没有异常机制,但是使用panic/recover 模式来处理错误。

panic可以在任何地方引发,但recover只有在defer调用的函数中有效。

func fn1()  {
	fmt.Println("fn1")
}
func fn2()  {
	panic("抛出一个异常")
}

func main() {
	fn1()
	fn2()
	fmt.Println("结束")
}
/**
	程序遇到panic会直接结束运行并抛出异常
*/

使用recover来接收异常

go里面没有try catch,所以使用panic和recover来进行异常处理

func fn1()  {
	fmt.Println("fn1")
}
func fn2()  {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("err:",err)
		}
	}()
	panic("抛出一个异常")
}

func main() {
	fn1()
	fn2()
	fmt.Println("结束")
}

处理异常的例子

func fn3(a,b int) int {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("error:",err)
		}
	}()
	return a/b
}

func main() {
	fmt.Println(fn3(3, 0))
	fmt.Println("结束")
}
/**
    error: runtime error: integer divide by zero
    0
    结束
*/

说白了就跟try catch一样,只不过需要在defer中用recover捕获异常,注意defer的一定是一个自执行函数

使用recover捕获异常之后,后续的代码是可以继续执行的

例子:模拟读取文件失败

func readFile(fileName string) error {
	if fileName == "main.go" {
		return nil
	}else {
		return errors.New("读取文件失败")
	}
}

func myFun() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("error:",err,"\n给管理员发邮件")
		}
	}()

	err := readFile("xxx.go")
	if err != nil {
		panic(err)
	}
}

func main() {
	myFun()
}

/**
    error: 读取文件失败 
    给管理员发邮件
*/

time包及日期函数

最拉夸的方式

func main() {
	now := time.Now()
	fmt.Println(now)	//2020-06-06 14:29:14.8076067 +0800 CST m=+0.011968301

	year := now.Year()
	month := now.Month()
	day := now.Day()
	hour := now.Hour()
	minute := now.Minute()
	second := now.Second()
	//year, month, day := now.Date()
	fmt.Printf("%d-%02d-%02d %02d:%02d:%02d",year, month, day,hour,minute,second)		//2020-06-06 14:38:23
}

格式化

需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S
而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)

  • 2006 年
  • 01 月
  • 02 日
  • 03 时 12小时制 15 24小时制
  • 04 分
  • 05 秒
func main() {
	now := time.Now()
	fmt.Println(now)	//2020-06-06 14:44:39.9233244 +0800 CST m=+0.007976001

	nowFormat1 := now.Format("2006-01-02 03:04:05")
	fmt.Println(nowFormat1)						//2020-06-06 02:42:50

	nowFormat2 := now.Format("2006-01-02 15:04:05")
	fmt.Println(nowFormat2)						//2020-06-06 14:44:39

}

获取当前时间戳

时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp) 。

func main() {
	now := time.Now()
	timeStamp := now.Unix()
	fmt.Println(timeStamp)		//1591426031
}

将时间戳转化为日期格式

func main() {
	now := time.Now()
	timeStamp := now.Unix()
	fmt.Println(timeStamp)		//1591426031
	//将时间戳转化为日期格式
	fmt.Println(time.Unix(timeStamp, 0).Format("2006-01-02 15:04:05"))	//2020-06-06 14:51:14
}

将日期转换为时间戳

顺便复习一下recover和panic

func main() {
	str := "2020-06-06 14:51:14"	
	tmp := "2006-01-02 15:04:05"
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("时间转换错误", err)
            //str := "2020/06-06 14:51:14"	
			//时间转换错误 parsing time "2020/06-06 14:51:14" as "2006-01-02 15:04:05": cannot parse "/06-06 14:51:14" as "-"
		}
	}()
	location, err := time.ParseInLocation(tmp, str, time.Local)
	if err != nil {
		panic(err)
	} else {
		unix := location.Unix()
		fmt.Println("时间戳是", unix)		//时间戳是 1591426274
	}
}

时间间隔类型常量

1、time包中定义的时间间隔类型的常量如下:
const (
Nanosecond Duration = 1
Microsecond 		= 1000 * Nanosecond
Millisecond			= 1000 * Microsecond
Second				= 1000 * Millisecond
Minute 				= 60 * Second
Hour				= 60 * Minute
)

func main(){
    // fmt.Println(time.Millisecond) //1ms
	// fmt.Println(time.Second)	//1s
}

时间操作函数

Add

时间+时间间隔

func (t Time) Add(d Duration) Time

Sub 时间差

Equal 比较时间,会考虑时区

定时器

使用time.NewTicker(时间间隔)来设置定时器

func main(){
    ticker := time.NewTicker(time.Second)
	n := 0
	for t := range ticker.C{
		n++
		if n > 5 {			//一共执行5次任务
			ticker.Stop()	//停止计时器
			return
		}
		fmt.Println(t)
		fmt.Println("offer")
	}
}

使用time.Sleep()休眠

func main(){
    for i := 5 ;i > 0 ;i-- {
		fmt.Println("offer")
		time.Sleep(time.Second)		//休眠1秒,相当于TimeUnit.SECOND.sleep(1)
	}
}

指针(童年阴影)

指针也是一个变量,但它是一种特殊的变量,它存储的数据不是一个普通的值,而是另一个变量的内存地址。

func main() {
	a := 10
	p := &a
	fmt.Printf("a: %v,%T,%p\n",a,a,&a)
	fmt.Printf("p: %v,%T,%p\n",p,p,&p)
    fmt.Printf("%v",*p)					//*p : 取出这个地址对应的值,即a的值
}
/**
	a: 10,int,0xc00000a0a8
	p: 0xc00000a0a8,*int,0xc000006028
	10
*/

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行取地址操作。Go 语言中的值类型(int、 float、 bool、 string、array、struct) 都有对应的指针类型,如: *int、 *int64、 *string 等。

简单例子

func fun1(x int) {
	x = 20		//值传递
}

func fun2(x *int) {
	*x = 40		//引用传递
}

func main() {
	a := 10
	fun1(a)
	fmt.Println(a)		//10
	fun2(&a)
	fmt.Println(a)		//40
}

指针也是引用类型

这样写会报错

var a *int
*a = 100
fmt.Println(*a)

使用new关键字创建初始化指针变量

new在实际开发中基本用不到**(shit! , java程序员直呼外行)**

func main() {
	a := new(int)	//a是一个指针变量 类型是*int的指针类型 指针变量对应的值是0
	fmt.Printf("%v--%T--%v",a,a,*a)		//0xc00000a0a8--*int--0
}

make函数分配内存

主要用于切片,map,channel的内存创建

make和new的区别

  • 二者都是用来做内存分配的。
  • make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
  • 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

结构体(害怕)

type自定义类型与类型别名的区别

func main() {
	var a myInt = 10
	fmt.Printf("%v,%T\n",a,a)	//10,main.myInt

	var b myFloat = 12.3
	fmt.Printf("%v,%T\n",b,b)	//12.3,float64
}

结构体的定义

使用type和struct关键字来定义结构体,具体代码格式如下

type 类型名 struct {
	字段名 字段类型
	字段名 字段类型
}

其中:

  • ==类型名:==表示自定义结构体的名称,在同一个包内不能重复。
  • ==字段名:==表示结构体字段名。结构体中的字段名必须唯一。
  • ==字段类型:==表示结构体字段的具体类型。

我们定义一个Person结构体

注意,结构体名字首字母可以大写也可以小写,小写代表他是私有的,字段也是一样的

type Person struct {
	name string
	age int
	gender string
}

结构体实例化

第一种方法 var

(小声BB:相当于java里面的setter)

结构体类型

var 结构体实例 结构体类型
func main(){
	var p1 Person
	p1.name = "张三"
	p1.age = 20
	p1.gender = "男"
	fmt.Printf("%v--%T\n",p1,p1)	//{张三 20 男}--main.Person
	fmt.Printf("%#v--%T",p1,p1)		//main.Person{name:"张三", age:20, gender:"男"}--main.Person
}

%#v代表详细信息(相当于toString)

第二种方式 new

(java菜逼直呼内行)

指针类型

func main(){
    var p2  = new(Person)
	p2.name = "李四"
	p2.age = 20
	p2.gender = "男"
	fmt.Printf("%#v--%T",p2,p2)		//&main.Person{name:"李四", age:20, gender:"男"}--*main.Person
}

从打印的结果中我们可以看出p2是一个结构体指针。
注意:在Golang中支持对结构体指针直接使用.来访问结构体的成员。p2.pname = "张三”其实在底层是(*p2).name = "张三”

第三种方式 直接结构体地址赋值

(无参构造器?)

指针类型

func main(){
    p3 := &Person{}
	p3.name = "花玲"
	p3.age = 18
	p3.gender = "女"
	fmt.Printf("%#v--%T",p3,p3)		//&main.Person{name:"花玲", age:18, gender:"女"}--*main.Person
}

第四种方式 键值对赋值

(有参构造器?)

结构体类型

func main(){
    p4 := Person{
		name:   "花玲",
		age:    18,
		gender: "女",
	}
	fmt.Printf("%#v--%T",p4,p4)		//main.Person{name:"花玲", age:18, gender:"女"}--main.Person
}

第五种方式 指针加赋值

指针类型

func main(){
    p5 := &Person{
		name:   "花玲",
		age:    18,
		gender: "女",
	}
	fmt.Printf("%#v--%T", p5, p5) //&main.Person{name:"花玲", age:18, gender:"女"}--*main.Person
}

注意,初始化的时候可以只给部分字段赋值,剩下的就是默认值,0值或空值

第六种方式 不加字段名,但是要一一对应

指针类型

func main(){
    p6 := &Person{
		"花玲",
		18,
		"女",
	}
	fmt.Printf("%#v--%T", p6, p6)	//&main.Person{name:"花玲", age:18, gender:"女"}--*main.Person
}

结构体方法和接收者

在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。

所谓方法就是定义了接收者的函数。

接收者的概念就类似于其他语言中的this或者self。

方法定义的格式如下

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
	函数体
}

其中:

  • **接收者变量:**接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、 this 之类的命名。例如,Person 类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。
  • **接收者类型:**接收者类型和参数类似,可以是指针类型和非指针类型。

定义一个Person结构体,定义一个方法打印person的信息

type Person2 struct {
	name   string
	age    int
	gender string
	height int
}

func (p Person2) PrintInfo() {
	fmt.Printf("姓名:%v 年龄:%v", p.name, p.age)
}

func main() {
	p1 := Person2{
		name:   "花玲",
		age:    18,
		gender: "女",
		height: 165,
	}
	p1.PrintInfo()	//姓名:花玲 年龄:18
}

定义一个方法,修改对象信息

注意,想要修改属性,接收者类型必须指定为结构体指针

当然,不修改结构体中的属性的话,不用指定为结构体指针

func (p Person2) PrintInfo() {
	fmt.Printf("姓名:%v 年龄:%v\n", p.name, p.age)
}
//这里
func (p *Person2) SetName(name string)  {
	p.name = name
}

func main() {
	p1 := Person2{
		name:   "花玲",
		age:    18,
		gender: "女",
		height: 165,
	}
	p1.PrintInfo()			//姓名:花玲 年龄:18
	p1.SetName("老婆")
	p1.PrintInfo()			//姓名:老婆 年龄:18
}

注意:结构体实例是独立的,不会相互影响

结构体嵌套和继承

结构体的匿名字段

就是不给字段取名字,直接定义类型

type person struct {
	string
	int
}

结构体嵌套

听起来好高端,就是DI

type User struct {
	username string
	password string

	address Address	//表示User结构体里面嵌套了Address结构体
}

type Address struct {
	name string
	phone string
	city string
}

func main() {
	var u User
	u.username = "小明"
	u.password = "123456"
	u.address = Address{
		name:  "小明",
		phone: "13996459090",
		city:  "重庆",
	}
    /**
    * 也可以这样
    * u.address.name = "小明"
	* u.address.phone = "13996459090"
	* u.address.city = "重庆"
    */	
	fmt.Println(u)	//{小明 123456 {小明 13996459090 重庆}}
}

当然,你可以写匿名的嵌套结构体,比如u.city = "重庆",由于User结构体本身并没有city这个字段,所以赋值的时候他会去user里面的嵌套结构体里找这个字段并给他赋值

关于嵌套结构体的字段名冲突

  • 如果结构体中有和嵌套结构体中相同的某一字段,那么赋值的时候会给结构体本身的字段赋值
  • 如果有两个嵌套结构体,他们有相同的字段,那么赋值的时候会报错,指定是哪一个嵌套结构体就行了u.Address.Addtime = "2006-01-01"

结构体的继承

通过匿名嵌套结构体实现继承

就这??虽然拉胯,不过确实是实现了继承

type Animal struct {
	name string
}
func (a Animal) run() {
	fmt.Printf("%v在运动\n",a.name)
}

type Dog struct {
	age int
	Animal
}
func (d Dog) shut()  {
	fmt.Printf("%v在叫\n",d.name)
}

func main() {
	var dog Dog
	dog.age = 2
	dog.name = "旺财"
	dog.run()			//旺财在运动
	dog.shut()			//旺财在叫
}

结构体序列化与反序列化Json

注意,想要将字段转换成json的话,字段必须是公有的(即首字母大写)

GolangJSON序列化是指把结构体数据转化成JSON格式的字符串,Golang JSON的反序列化是指把JSON数据转化成Golang中的结构体对象

序列化

通过json.Marshal()序列化

type Student struct {
	Id int
	Gender string
	Name string
	Sno string
}

func main() {
	s1 := Student{
		Id:     12,
		Gender: "男",
		Name:   "张三",
		Sno:    "s001",
	}
	fmt.Println(s1)					//{12 男 张三 s001}
	marshal, _ := json.Marshal(s1)
	jsonStr := string(marshal)
	fmt.Println(jsonStr)			//{"Id":12,"Gender":"男","Name":"张三","Sno":"s001"}
}

反序列化

type Student struct {
	Id int
	Gender string
	Name string
	Sno string
}

func main() {
	var str = `{"Id":12,"Gender":"男","Name":"张三","Sno":"s001"}`		//注意此处是`` 而不是''
	var s1 Student
	err := json.Unmarshal([]byte(str),&s1)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%#v",s1)		//main.Student{Id:12, Gender:"男", Name:"张三", Sno:"s001"}

}

结构体标签Tag

我想在json序列化之后将json中的字段首字母改为小写

type Student struct {
	Id int	`json:"id"`
	Gender string	`json:"gender"`
	Name string	`json:"name"`
	Sno string	`json:"xxx"`
}

func main() {
	s1 := Student{
		Id:     12,
		Gender: "男",
		Name:   "张三",
		Sno:    "s001",
	}
	fmt.Println(s1)					//{12 男 张三 s001}
	marshal, _ := json.Marshal(s1)
	jsonStr := string(marshal)
	fmt.Println(jsonStr)			
	//{"Id":12,"Gender":"男","Name":"张三","Sno":"s001"}	原来的
	//{"id":12,"gender":"男","name":"张三","xxx":"s001"}	加了Tag
}

嵌套结构体的序列化与反序列化

序列化

type Student struct {
	Id int
	Gender string
	Name string

}
type Class struct {
	Title string
	Students []Student
}
func main() {
    class := Class{
		Title:   "6班",
		Students: make([]Student, 0,200),
	}
	for i := 0; i < 10; i++ {
		s := Student{
			Id:     i,
			Gender: "女",
			Name:   fmt.Sprintf("学生_%v",i),
		}
		class.Students = append(class.Students,s)
	}
	fmt.Println(class)
	marshal, _ := json.Marshal(class)
	jsonStr := string(marshal)
	fmt.Println(jsonStr)
}
/**
{6班 [{0 女 学生_0} {1 女 学生_1} {2 女 学生_2} {3 女 学生_3} {4 女 学生_4} {5 女 学生_5} {6 女 学生_6} {7 女 学生_7} {8 女 学生_8} {9 女 学生_9}]}
{"Title":"6班",
"Students":[{"Id":0,"Gender":"女","Name":"学生_0"},{"Id":1,"Gender":"女","Name":"学生_1"},{"Id":2,"Gender":"女","Name":"学生_2"},{"Id":3,"Gender":"女","Name":"学生_3"},{"Id":4,"Gender":"女","Name":"学生_4"},{"Id":5,"Gender":"女","Name":"学生_5"},{"Id":6,"Gender":"女","Name":"学生_6"},{"Id":7,"Gender":"女","Name":"学生_7"},{"Id":8,"Gender":"女","Name":"学生_8"},{"Id":9,"Gender":"女","Name":"学生_9"}]}
*/

使用json解析工具可以看到,我们是解析成功的

Go,冲了_第5张图片

反序列化

type Student struct {
	Id int
	Gender string
	Name string

}
type Class struct {
	Title string
	Students []Student
}

func main(){
    jsonStr := `{"Title":"6班","Students":[{"Id":0,"Gender":"女","Name":"学生_0"},{"Id":1,"Gender":"女","Name":"学生_1"},{"Id":2,"Gender":"女","Name":"学生_2"},{"Id":3,"Gender":"女","Name":"学生_3"},{"Id":4,"Gender":"女","Name":"学生_4"},{"Id":5,"Gender":"女","Name":"学生_5"},{"Id":6,"Gender":"女","Name":"学生_6"},{"Id":7,"Gender":"女","Name":"学生_7"},{"Id":8,"Gender":"女","Name":"学生_8"},{"Id":9,"Gender":"女","Name":"学生_9"}]}`
	var class Class
	err := json.Unmarshal([]byte(jsonStr), &class)
	if err != nil {
		fmt.Println(err)
	}else {
		fmt.Println(class)
	}
}
//{6班 [{0 女 学生_0} {1 女 学生_1} {2 女 学生_2} {3 女 学生_3} {4 女 学生_4} {5 女 学生_5} {6 女 学生_6} {7 女 学生_7} {8 女 学生_8} {9 女 学生_9}]}

go mod以及go包

  • 包( package)是多个Go源码的集合,是一种高级的代码复用方案,Go 语言为我们提供了很多内置包,如fmt、strconv、 strings、 sort、 errors、 time、 encoding/json、 OS、io 等。
  • Golang中的包可以分为三种: 1.系统内置包 2.自定义包 3.第三方包
  • 系统内置包: Golang语言给我们提供的内置包,引入后可以直接使用,如fmt、strconv、strings、sort、errors、 time、encoding/json、 OS、io 等。
  • 自定义包:开发者自己写的包
  • 第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面给大家介绍的"github.com/shopspring/decimal"包解决float精度丢失问题。

go包管理工具go mod

在Golang1.11版本之前如果我们要自定义包的话必须把项目放在GOPATH目录。Go1.11版本之后无需手动配置环境变量,使用go mod管理项目,也不需要非得把项目放到GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目, Go1.13以后可以彻底不要GOPATH了。

go mod init 初始化项目

实际项目开发中我们首先要在我们项目目录中用go mod命令生成一个go.mod文件管理我们项目的依赖。
比如我们的golang项目文件要放在了某个文件夹,这个时候我们需要在这个文件夹里面使用go mod命令生成一个go.mod文件

go mod简介及参数

Go,冲了_第6张图片

Go,冲了_第7张图片

某些同学用golan的时候import会爆红,但是又能正常使用包里面的方法

看看你的go moudles有没有开启

Go,冲了_第8张图片

另外,通过命令修改go的module开启,以及国内代理设置

go env # 查看go的环境设置

设置命令

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

Go,冲了_第9张图片

包名很长,给包取一个别名

import (
	"fmt"
	T "gomod/calc"
)

func main() {
	sum := T.Add(10,2)
	fmt.Println(sum)
}

init()函数

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。init()函 数在程序运行时自动被调用执行,不能在代码中主动调用它。

Go,冲了_第10张图片

在运行时,被最后导入的包会最先初始化并调用其init()函数,如下图示:

Go,冲了_第11张图片

import (
	"fmt"
	T "gomod/calc"
)

func init() {							//系统自动执行,不需要手动调用
	fmt.Println("init........")
}

func main() {
	sum := T.Add(10,2)
	fmt.Println(sum)	
}
/**
init........
12
*/

go中使用第三方包

我们可以在https://pkg.go.dev/ 查找看常见的golang第三方包

找到我们需要下载安装的第三方包的地址

比如前面给大家演示的解决float精度损失的包decimal https://github.com1/shopspring/decimal

安装这个包

第一种方法

go get 包名称 (全局)

go get github.com/shopspring/decimal

第二种方法

go mod download (全局)

第三种方式

go mod vender 将依赖复制到当前项目的vender下(本项目)

引入这个包

可以在第三方包的文档里面看到该怎么引入

一般是import go get后的地址

哎反正你import包就完事儿了,golan会帮你下载的

接口

接口是什么在这里不做阐述,java开发的同学应该很清楚,接口就是一系列动作的规范

接口的定义

  • 在Golang中接口(interface) 是一种类型,一种抽象的类型。接口(interface) 是一-组函数method的集合,Golang 中的接口不能包含任何变量。
  • 在Golang中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想
  • Golang中的接口也是一种数据类型,不需要显示实现。只需要-一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口。
type 接口名 interface{
    方法名1( 参数列表1 )	返回值列表1
	方法名2( 参数列表2 )	返回值列表2
	...
}
  • **接口名:**使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • **方法名:**当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package) 之外的代码访问。
  • 参数列表,返回值列表: 参数列表和返回值列表中的参数变量名可以省略

如果接口里面有方法,必须通过结构体或者自定义类型实现接口

哎,就是实现类 implements,懂?

哎,乏味

type Usber interface {
	start()
	stop()
}

type Phone struct {
	Name string

}

func (p Phone) start()  {
	fmt.Println(p.Name,"启动")
}
func (p Phone) stop()  {
	fmt.Println(p.Name,"关机")
}

func main()  {
	var p1 Usber = Phone{Name: "华为"}	//就和java一样,接口接收实现类
	p1.start()							 //华为 启动
}

当然,众所周知,接口里面没有定义的方法,是不能调用的

比如你的实现类中有自己的方法,通过接口是不能调用到的,但是为什么要这样做呢?接口都是行为规范了.

比如java中的List接口中没有定义LinkedList的相关方法,所以如果你用List去接,是调用不到的,你要用LinkedList去接.

这就变成了直接使用实现类(结构体).

再来看一个例子

java开发的同学都知道,其实应该把各个接口和实现类分开放到不同的go中,但是这里方便展示就放一起了

这里打的电脑的work方法接收的参数是一个接口类型,实际上是体现了多态,这里不多赘述

type Usber interface {
	start()
	work()
	stop()
}
//电脑
type Computer struct {

}
func (c Computer) work(usb Usber)  {
	usb.start()
	usb.work()
	usb.stop()
}
//手机
type Phone struct {
}
func (p Phone) start()  {
	fmt.Println("手机启动")
}
func (p Phone) stop()  {
	fmt.Println("手机关机")
}
func (p Phone) work(){
	fmt.Println("手机工作")
}
//相机
type Camera struct {

}
func (c Camera) start()  {
	fmt.Println("相机启动")
}
func (c Camera) stop()  {
	fmt.Println("相机关机")
}
func (c Camera) work(){
	fmt.Println("相机工作")
}

func main()  {
	var computer = Computer{}
	var phone Usber = Phone{}
	var camera Usber = Camera{}
	computer.work(phone)
	computer.work(camera)
}
/**
手机启动
手机工作
手机关机
相机启动
相机工作
相机关机
*/

空接口

Serializable?其实有差别

空接口代表没有任何约束,可以接收任意类型

空接口作为函数的参数进行

//空接口测试
func show(a interface{})  {
	fmt.Printf("值:%v,类型:%T",a,a)
}

func main()  {
	str := "这是一个测试"
	show(str)				//值:这是一个测试,类型:string
}

map的值实现空接口

这样就可以往这个map中传任意类型了,就好比Map

func main(){
    map1 := make(map[string]interface{})
	map1["1"] = "string类型"
	map1["2"] = 2
	map1["3"] = []string{
		"切片类型1","切片类型2",
	}
	show(map1)
}
/**
值:map[1:string类型 2:2 3:[切片类型1 切片类型2]],类型:map[string]interface {}
*/

切片值实现空接口

同理,Object类型的list

func main(){
    arr := make([]interface{},3,3)
	arr[0] = "string"
	arr[1] = 1
	arr[2] = 2.84
	show(arr)
}
/**
值:[string 1 2.84],类型:[]interface {}
*/

类型断言

相当于instanceof

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:

x.(T)
  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第- 个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则断言成功,false则失败

func main(){
    var a interface{} = "你好"
	v,ok := a.(string)
	if ok {
		fmt.Println("a是一个string类型",v)
	}else {
		fmt.Println("断言失败")
	}
}
/**
a是一个string类型 你好
*/

注意:类型.(type)只能结合switch语句使用

定义一个方法,可以传入任意数据类型,然后根据不同的类型实现不同的功能

func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string, value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type! ")
	}
}

再比如

func (c Computer) work(usb Usber)  {
	//根据传入接口的不同类型执行不同的逻辑
	switch usb.(type) {
	case Phone:
		usb.start()
	case Camera:
		usb.stop()
	}
}
func main()  {
	var computer = Computer{}
	var phone Usber = Phone{}
	var camera Usber = Camera{}
	computer.work(phone)
	computer.work(camera)
}
/**
手机启动
相机关机
*/

结构体值接收者和指针接收者实现接口的区别

值接收者

如果结构体中的方法是值接收者,那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量

type Usber interface {
	start()
	work()
	stop()
}

//手机
type Phone struct {
}
func (p Phone) start()  {	//值接收者
	fmt.Println("手机启动")
}
func (p Phone) stop()  {
	fmt.Println("手机关机")
}
func (p Phone) work(){
	fmt.Println("手机工作")
}

func main() {
	//结构体值接收者,实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量
	var p1 Usber = Phone{}
	var p2 Usber = &Phone{}
	p1.start()
	p2.stop()
}

指针接收者

结构体指针接收者,实例化后只有结构体指针类型才可以赋值给接口变量

type Usber interface {
	start()
	work()
	stop()
}

//手机
type Phone struct {
}
func (p *Phone) start()  {	//值接收者
	fmt.Println("手机启动")
}
func (p *Phone) stop()  {
	fmt.Println("手机关机")
}
func (p *Phone) work(){
	fmt.Println("手机工作")
}

func main() {
	//结构体指针接收者,实例化后只有结构体指针类型才可以赋值给接口变量
	var p1 Usber = Phone{}		//报错	Phone does not implement Usber (start method has pointer receiver)
	var p2 Usber = &Phone{}
	p1.start()
	p2.stop()
}

接口方法有返回值的情况

哎其实差不多,不过因为要修改结构体属性值,所以注意方法要使用指针类型

type Animal interface {
	SetName(string)
	GetName() string
}

type Dog struct {
	Name string
}
func (d *Dog) SetName(name string)  {
	d.Name = name
}
func (d Dog) GetName() (name string) {
	return d.Name
}

type Cat struct{
	Name string
}

func (c *Cat) SetName(name string) {
	c.Name = name
}

func (c Cat) GetName() string {
	return c.Name
}

func main() {
	var dog Animal = &Dog{Name: "wangcai"}
	var cat Animal = &Cat{Name: "xiaohei"}
	fmt.Println(dog.GetName())
	dog.SetName("旺财")
	fmt.Println(dog.GetName())

	fmt.Println(cat.GetName())
	cat.SetName("小黑")
	fmt.Println(cat.GetName())
}
/**
wangcai
旺财
xiaohei
小黑	
*/

一个结构体实现多个接口,接口嵌套

多态?

使用接口嵌套达到结构体实现多个接口的目的

当然,尽量遵守单一职责设计原则,一个接口尽量不耦合其它接口,另外定义一个接口来组合已有的接口

type Animal interface {
	SetName(string)
	GetName() string
}
type Action interface {
	run()
	sleep()
}
//定义一个接口,要求实现该接口的结构体实现包含接口的所有方法
type MovingAnimal interface {
	Animal
	Action
}

type Dog struct {
	Name string
}
func (d *Dog) SetName(name string)  {
	d.Name = name
}
func (d Dog) GetName() (name string) {
	return d.Name
}

func (d Dog) run() {
	fmt.Println(d.Name,"在奔跑")
}

func (d Dog) sleep() {
	fmt.Println(d.Name,"在睡觉")
}

func main() {
	var dog MovingAnimal = &Dog{Name: "wangcai"}
	fmt.Println(dog.GetName())
	dog.SetName("旺财")
	fmt.Println(dog.GetName())
	dog.run()
	dog.sleep()
}
/**
wangcai
旺财
旺财 在奔跑
旺财 在睡觉
*/

空接口和类型断言的使用细节

空接口类型是不能进行索引或者说拿到结构体中的值的,通过类型断言就可以做到,具体看下面的示例

type Address struct {
	Name  string
	Phone int
}

//Golang中空接口和类型断言使用细节
func main() {
	var userinfo = make(map[string]interface{})
	userinfo["username"] = "花玲"
	userinfo["age"] = 18
	userinfo["hobby"] = []string{"吃饭", "睡觉", "我"}

	fmt.Println(userinfo["username"])	//花玲
	fmt.Println(userinfo["age"])		//18

	//fmt.Println(userinfo["hobby"][1])	//type interface {} does not support indexing

	address := Address{
		Name:  "ky",
		Phone: 123456789,
	}
	userinfo["address"] = address
	//fmt.Println(userinfo["age"].Name)	//type interface {} is interface with no methods

	//解决方案,类型断言
	hobby,_ := userinfo["hobby"].([]string)
	fmt.Println(hobby[2])	//我

	addr,_ := userinfo["address"].(Address)
	fmt.Println(addr.Name,addr.Phone)	//ky 123456789
}

goroutine协程

为什么要使用goroutine

**需求: **要统计1-10000006的数字中那些是素数,并打印这些素数?
素数: 就是除了1和它本身不能被其他数整除的数

实现方法:

  1. 传统方法,通过一个for循环判断各个数是不是素数
  2. 使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这个时候就用到了goroutine
  3. goroutine 结合channel

Golang中的协程(goroutine) 以及主线程

golang中的主线程: ( 可以理解为线程/也可以理解为进程),在一个Golang程序的主线程上可以起多个协程Golang 中多协程可以实现并行或者并发。
协程: 可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。Golang的一大特色就是从语言层面原生支持协程,在函数或者方法前面加go 关键字就可创建一个协程。可以说Golang中的协程就是goroutine。

Go,冲了_第12张图片

Golang中的多协程有点类似其他语言中的多线程。

多协程和多线程: Golang中每个goroutine (协程)默认占用内存远比Java、C的线程少。
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB左右) ,一个goroutine (协程)占用内存非常小,只有2KB左右,多协程goroutine切换调度开销方面远比线程要少。
这也是为什么越来越多的大公司使用Golang的原因之一。

Goroutine的使用以及sync.WaitGroup

并行执行需求:

  • 在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔50毫秒秒输出"你好golang"
  • 在主线程中也每隔50毫秒输出"你好golang",输出10次后,退出程序,要求主线程和goroutine同时执行。
func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() hello golang")
		time.Sleep(time.Millisecond * 50)
	}
}

func main() {
	go test()	//开启一个协程
	for i := 0; i < 10; i++ {
		fmt.Println("main() hello golang")
		time.Sleep(time.Millisecond * 50)
	}
}

问题?

如果主线程中任务的执行速度比协程中的任务执行速度快,会出现什么问题?

当主线程执行结束,协程无论是否执行完毕,都会停止执行.

Go,冲了_第13张图片

使用sync.WaitGroup

这是个什么东西?

java开发的同学应该知道,其实这就是个CountdownLatch,等我娓娓道来

var wg sync.WaitGroup

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() hello golang--",i)
		time.Sleep(time.Millisecond * 100)
	}
	wg.Done()	//计数器减一
}

func main() {
	wg.Add(1)	//计数器加一
	go test()	//开启一个协程
	for i := 0; i < 10; i++ {
		fmt.Println("main() hello golang--",i)
		time.Sleep(time.Millisecond * 50)
	}
	wg.Wait()
	fmt.Println("主线程执行结束")
}

其中

wg.Add(1) 相当于CowndownLatch的初始化为1

wg.Done() 相当于CowndownLatch的countDown()方法

wg.wait() 相当于CowndownLatch的await()

这样说好理解了吗?

当然,可以开启多个协程任务

这里我们开启了5个协程去执行test方法,每个test方法里打印10条语句

var wg sync.WaitGroup

func test(x int) {
	defer wg.Done()
	for i := 1; i <= 10; i++ {
		fmt.Println("协程--",x,"--",i)
	}
}
func main() {
	for i := 1; i <= 5 ; i++ {
		go test(i)
		wg.Add(1)
	}
	wg.Wait()
}

设置golang并行运行时候占用的cpu数量

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU逻辑核心数。

一个需求

统计1-120000内的素数,for循环实现

func main() {
	start := time.Now().Unix()
	for i := 2; i <= 120000; i++ {
		flag:=true
		for j := 2; j <= i/2 ; j++ {
			if i%j == 0 {
				flag = false
				break
			}
		}
		if flag {
			//fmt.Print(i," ")
		}
	}
	end := time.Now().Unix()
	fmt.Println(end-start)	// j
}

开启多个协程

func cal(n int) {
	start := (n-1)*30000 + 1
	end := n * 30000
	defer wg.Done()
	for i := start; i <= end; i++ {
		if i == 1 {
			continue
		}
		flag := true
		for j := 2; j <= i/2; j++ {
			if i%j == 0 {
				flag = false
				break
			}
		}
		if flag {
			fmt.Print(i,"是素数\n")
		}
	}
}

func main() {
	//统计1-120000内的素数,协程实现
	start := time.Now().Unix()
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go cal(i)
	}
	wg.Wait()
	end := time.Now().Unix()
	fmt.Println(end - start)	//2毫秒
}

channel管道

  • 管道是Go语言在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一- 个goroutine发送特定值到另-个 goroutine 的通信机制。

  • Go语言的并发模型是CSP ( Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

  • Go语言中的管道(channel) 是一种特殊的类型。管道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型, 一种引用类型。声明管道类型的格式如下:

var 变量 chan 元素类型

举几个例子:

var ch1 chan int 	//声明一个传递整型的管道
var ch2 chan bool	//声明一个传递布尔型的管道
var ch3 chan []int  //声明一个传递int切片的管道

创建channel

声明的管道后需要使用make函数初始化之后才能使用。

make(chan 元素类型,容量)
//创建一个能存储10个int 类型数据的管道
ch1 := make(chan int, 10)
//创建一一个能存储4个bool类上数据的管道
ch2 := make(chan bool, 4)
//创建一个 能存储3个[]int 切片类型数据的管道
ch3 := make(chan []int, 3)

channel操作

管道有``发送(send) 、接收(receive) 和关闭(close) 三种操作。 发送和接收都使用<-`符号的
现在我们先使用以下语句定义一个管道:

ch := make(chan int, 3)

1.发送(将数据放在管道内)

将一个值放进管道

ch <- 10	//把10发送到ch中

2.接收(从管道内取值)

x := <- ch	//从ch中取值并赋值给变量x

3.关闭管道

close(ch)

管道是引用数据类型

func main() {
	//1.创建channel
	ch := make(chan int, 3)

	//2.给管道里面发送数据
	ch <- 3
	ch <- 4
	ch <- 5

	//3.获取管道内容
	a := <-ch
	fmt.Println(a) //3
	<-ch //从管道里取值	//4
	c := <-ch
	fmt.Println(c)	//5
	ch <- 6
	ch <- 7
    
	//4.管道的容量,和长度
	fmt.Printf("值:%v,容量:%v,长度:%v",ch,cap(ch),len(ch))		//值:0xc00007e000,容量:3,长度2

	//6.管道阻塞
	ch1:=make(chan int,1)
	ch1 <- 2
	ch1 <- 3	//all goroutines are asleep - deadlock!

}

关于管道阻塞

  • 管道满了,添加(deadlock)
  • 管道空了,取值(deadlock)

在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告deadlock

管道的循环遍历

循环遍历管道数据
使用for range遍历通道,当通道被关闭的时候就会退出for range, 如果没有关闭管道就会报个错误

func main() {
	var ch1 = make(chan int, 10)
	for i := 0; i < 10; i++ {
		ch1 <- i
	}
	close(ch1)
	//for range循环遍历管道的值	管道没有key
	for v := range ch1 {
		fmt.Println(v) //如果不关闭管道会报错fatal error: all goroutines are asleep - deadlock!
	}
}

但是使用for循环不关闭管道不会有这个问题

我觉得细细想一下,也很科学,range的时候会去取"下一个值",但是for就不会,所以不会deadlock

func main(){
    ch2 := make(chan int,10)
	for i := 0; i < 10; i++ {
		ch2 <- i
	}

	for i := 0; i < 10; i++ {
		fmt.Println(<-ch2)
	}
}

总结

  • 使用forr时,记得关闭管道.
  • 如果不知道什么时候关闭管道就使用fori

goroutine结合channel

**需求1:**定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。

  1. 开启一个fn1的的协程给向管道inChan中写入100条数据
  2. 开启一个fn2的协程读取inChan中写入的数据
  3. 注意: fn1和fn2同时操作一一个管道
  4. 主线程必须等待操作完成后才可以退出
var wg sync.WaitGroup

//写数据
func fn1(ch chan int) {
	for i := 1; i <= 10; i++ {
		ch <- i
		fmt.Printf("[写入]数据%v成功\n", i)
		time.Sleep(time.Millisecond * 50)
	}
	close(ch)
	wg.Done()
}

//读数据
func fn2(ch chan int) {
	for v := range ch {
		fmt.Printf("[读取]数据%v成功\n", v)
		time.Sleep(time.Millisecond * 50)
	}
	wg.Done()
}

func main() {
	var ch = make(chan int, 10)
	wg.Add(1)
	go fn1(ch)
	wg.Add(1)
	go fn2(ch)
	wg.Wait()
	fmt.Println("退出........")
}
/**
[读取]数据1成功
[写入]数据1成功
[写入]数据2成功
[读取]数据2成功
[写入]数据3成功
[读取]数据3成功
[写入]数据4成功
[读取]数据4成功
[写入]数据5成功
[读取]数据5成功
[写入]数据6成功
[读取]数据6成功
[写入]数据7成功
[读取]数据7成功
[写入]数据8成功
[读取]数据8成功
[写入]数据9成功
[读取]数据9成功
[写入]数据10成功
[读取]数据10成功
退出........
*/

说明

管道是安全的,也许你写入数据很慢,读取数据很快,他读不到数据会阻塞,管道是安全的

gorutine结合channel实现统计素数

3个channel,太秀了,上代码

Go,冲了_第14张图片

var wg sync.WaitGroup

//存放初始数据
func putNum(inChannel chan int) {
	for i := 2; i < 120000; i++ {
		inChannel <- i
	}
	close(inChannel)
	wg.Done()
}

//统计素数
func primeNum(inChannel, primeChannel chan int, exitChan chan bool) {
	for num := range inChannel {
		flag := true
		for j := 2; j <= num/2; j++ {
			if num%j == 0 {
				flag = false
				break
			}
		}
		if flag {
			primeChannel <- num //num是素数
		}
	}

	//给exitChan里面放入一条数据
	exitChan <- true
	//close(primeChannel)		//如果一个channel关闭了就没法给这个channel发送数据了,所以我们不在这里close
	wg.Done()

}

//打印素数
func printPrime(primeChannel chan int) {
	for prime := range primeChannel {
		fmt.Println("得到一个素数", prime)
	}
	wg.Done()
}

func main() {
	start := time.Now().Unix()
	inChannel := make(chan int, 1000)
	primeChannel := make(chan int, 1000)
	exitChan := make(chan bool, 16) //标识primeChan close

	//存放数字的协程
	wg.Add(1)
	go putNum(inChannel)
	//统计素数的协程
	for i := 0; i < 16; i++ {
		wg.Add(1)
		go primeNum(inChannel, primeChannel, exitChan)
	}
	//打印素数的协程
	wg.Add(1)
	go printPrime(primeChannel)

	//判断exitChan是否存满值
	wg.Add(1)
	go func() {
		for i := 0; i < 16; i++ {
			<-exitChan //如果执行的速度比其他的协程快,这里会阻塞
		}
		close(primeChannel)
		wg.Done()
	}()

	wg.Wait()
	end := time.Now().Unix()
	fmt.Println("执行完毕........", end-start,"ms")
}

运行结果

Go,冲了_第15张图片

单向管道

有的时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收。

默认情况下,管道是双向的

func main(){
    ch := make(chan int,5)
	ch<-1
	ch<-2
	<-ch
	<-ch
}

声明为只读管道

func main() {
	ch := make(chan <- int,5)	//声明为只写
	ch<-1
	ch<-2
	<-ch	//receive from send-only type chan<- int
}

声明为只读管道

func main() {
	ch := make(<- chan int,5)	//声明为只读
	ch<-1	//send to receive-only type <-chan int
	ch<-2
	<-ch	
}

应用:一个管道两个协程,一个只读,一个只写

var wg sync.WaitGroup

func fn1(ch chan<- int) {
	for i := 0; i < 10; i++ {
		ch<-i
		fmt.Printf("[写入]数据%v成功\n",i)
		time.Sleep(time.Millisecond*50)
	}
	close(ch)
	wg.Done()
}
func fn2(ch <-chan int)  {
	for v := range ch {
		fmt.Printf("[读取]数据%v成功\n",v)
		time.Sleep(time.Microsecond*50)
	}
	wg.Done()
}

func main() {
	var ch = make(chan int,10)
	wg.Add(1)
	go fn1(ch)
	wg.Add(1)
	go fn2(ch)
	wg.Wait()
	fmt.Println("执行结束......")
}
/**
....
[写入]数据7成功
[读取]数据7成功
[写入]数据8成功
[读取]数据8成功
[写入]数据9成功
[读取]数据9成功
执行结束......
*/

select多路复用

以上这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。

为了应对这种场景,Go内置了select关键字,可以同时响应多个管道的操作。

select的使用类似于switch语句,它有一系列case分支一个默认的分支。每个case会对应一个管道的通信(接收或发送)过程。select 会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

select{
	case <-ch1:
    	...
	case data := <-ch2:
    	...
	case ch3<-data:
    	...
    default:
    	默认操作
}

select多路复用是结合for循环实现的

使用select多路复用的时候不需要关闭channel

func main(){
    //select多路复用
	for {
		select {
		case v:=<-inChan:
			fmt.Printf("从inChan中读取数据-%v\n",v)
			time.Sleep(time.Millisecond*50)
		case v:=<-stringChan:
			fmt.Printf("从stringChan中读取数据-%v\n",v)
			time.Sleep(time.Millisecond*50)
		default:
			fmt.Printf("数据获取完毕")
			return		//注意退出
		}
	}
}

goruntine recover解决协程中出现的panic

和之前我们处理异常的方式一样

func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Millisecond * 50)
		fmt.Println("hello world")
	}

}

func test() {
	//处理异常
	defer func() {
		err := recover()
		if err!=nil {
			fmt.Println("错误:",err)
		}
	}()
	//一个map
	var myMap map[int]string	//这里没有分配内存,所以必定抛异常,如果不处理程序就终止
	myMap[0] = "golan"

}

func main() {
	go sayHello()
	go test()
	time.Sleep(time.Second)
	fmt.Println("执行完毕....")
}

go并发安全和锁

互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex 类型只有两个公开的指针方法,Lock 和Unlock。Lock 锁定当前的共享资源,Unlock 进行解锁

同一时间只能有一个协程对资源进行访问

并行改串行

var count = 0
var wg sync.WaitGroup

var mutex sync.Mutex

func test() {
	defer wg.Done()
	mutex.Lock()		//加锁
	count++
	fmt.Println("count : ",count)
	time.Sleep(time.Millisecond)
	mutex.Unlock()		//解锁
}

func main() {
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go test()
	}
	wg.Wait()
	fmt.Println("执行结束....")
}

读写锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
其实,当我们对一个不会变化的数据只做“读"操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine 同时读取,都是可以的。
所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

因此,衍生出另外一-种锁,叫做读写锁
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法:

一组是对写操作的锁定和解锁,简称“写锁定”和“写解锁:

func (*RWMutex)Lock()
func (*RWMutex)Unlock()

另一组表示对读操作的锁定和解锁,简称为“读锁定”与“读解锁" :

func (*RWMutex)RLock()
func (*RWMutex)RUnlock()

读写锁示例

var wg sync.WaitGroup
var rwMutex sync.RWMutex

//写方法
func write() {
	defer wg.Done()
	rwMutex.Lock()
	fmt.Println("执行写操作")
	time.Sleep(time.Second * 2)
	rwMutex.Unlock()
}

//读方法
func read() {
	defer wg.Done()
	rwMutex.RLock()
	fmt.Println("----执行读操作")
	time.Sleep(time.Second * 2)
	rwMutex.RUnlock()
}

func main() {
	//开启10个协程执行读操作
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go read()
	}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	wg.Wait()
	fmt.Println("执行结束....")
}

反射

有时我们需要写一个函数, 这个函数有能力统一处理 各种值类型,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射。

  1. 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么?值是什么呢?
    1. 可以使用类型断言
    2. 可以使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息。
  2. 把结构体序列化成json字符串,自定义结构体Tab标签的时候就用到了反射
  3. ORM框架就用到了反射技术

反射是框架的灵魂, java开发的同学就应该知道,spring等框架底层大量用到了反射

**ORM:**对象关系映射( Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。

反射的基本介绍

反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

go可以实现的功能

  • 反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型类别
  • 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法。
  • 通过反射,可以修改变量的值,可以调用关联的方法

go语言中的变量是分为两部分的

**类型信息:**预先定义好的元信息

**值信息:**程序运行过程中可动态变化

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。

在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个重要函数来获取任意对象的Value和Type

reflect.TypeOf()获取任意值的类型对象

在Go语言中,使用reflect.TypeOf()函数可以接受任意interface{}参数,可以获得任意值的类型对象(reflect.Type) ,程序通过类型对象可以访问任意值的类型信息。

type myInt int
type Person struct {
	Name string
	Age int
}

//反射获取任意变量的类型
func reflectFn(x interface{}) {
	typeOf := reflect.TypeOf(x)
	fmt.Println(typeOf)
}
func main() {
	a := 10
	b := 12.3
	c := true
	d := "你好"
	reflectFn(a)	//int
	reflectFn(b)	//float64
	reflectFn(c)	//bool
	reflectFn(d)	//string

	var num myInt = 2
	var person = Person{
		Name: "花玲",
		Age:  18,
	}
	reflectFn(num)	//main.myInt
	reflectFn(person)	//main.Person

	var h = 24
	reflectFn(&h)	//*int
}

type Name和type Kind

在反射中关于类型还划分为两种:类型(Type)种类(Kind) 。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind) 就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind) 。

举 个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。

type myInt int
type Person struct {
	Name string
	Age int
}

//反射获取任意变量的类型
func reflectFn(x interface{}) {
	typeOf := reflect.TypeOf(x)
	fmt.Printf("类型:%v,Name:%v,Kind:%v\n",typeOf,typeOf.Name(),typeOf.Kind())
}
func main() {
	a := 10
	b := 12.3
	c := true
	d := "你好"
	reflectFn(a)	//类型:int,Name:int,Kind:int
	reflectFn(b)	//类型:float64,Name:float64,Kind:float64
	reflectFn(c)	//类型:bool,Name:bool,Kind:bool
	reflectFn(d)	//类型:string,Name:string,Kind:string

	var num myInt = 2
	var person = Person{
		Name: "花玲",
		Age:  18,
	}
	reflectFn(num)	//类型:main.myInt,Name:myInt,Kind:int
	reflectFn(person)	//类型:main.Person,Name:Person,Kind:struct

	var h = 24
	reflectFn(&h)	//类型:*int,Name:,Kind:ptr
}

Name:类型名称 Kind():底层类型

reflect.ValueOf()

reflect.valueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value 与原始值之间可以互相转换

reflect.Value类型提供的获取原始值的方法如下:

Go,冲了_第16张图片

//反射获取任意变量的值
func reflectValue(x interface{}) {
	//fmt.Println(x)
	//num := x.(int)
	//sum := num + 10
	//fmt.Println(sum)	//23

	//反射获取变量的原始值
	v:=reflect.ValueOf(x)
	sum := v.Int() + 10
	fmt.Println(sum)	//23
}
func main() {
	var a = 13
	reflectValue(a)
}
//反射获取任意变量的值
func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	kind := v.Kind()
	switch kind {
	case reflect.Int:
		fmt.Printf("int类型的原始值%v\n",v.Int())
	case reflect.Float32:
		fmt.Printf("float32类型的原始值:%v\n",v.Float())
	case reflect.String:
		fmt.Printf("string类型的原始值:%v\n",v.String())
	default:
		fmt.Printf("还没有判断该类型\n")
	}
}
func main() {
	var a int64 = 13
	var b float32 = 12.3
	var c string = "你好golang"
	reflectValue(a)		//还没有判断该类型
	reflectValue(b)		//float32类型的原始值:12.300000190734863	精度损失
	reflectValue(c)		//string类型的原始值:你好golang
}

利用reflect.ValueOf()修改原始值

func reflectSetValue(x interface{}) {
	//*x = 120	//错误写法

	//v := x.(*int64)	//类型断言可以
	//*v = 120

	v:=reflect.ValueOf(x)
	//kind := v.Kind()     //ptr
	//k := v.Elem().Kind() //int64
	switch v.Elem().Kind() {
	case reflect.Int64:
		v.Elem().SetInt(123)
	case reflect.String:
		v.Elem().SetString("go你好")
	default:
		fmt.Println("还未识别该类型")
	}

}
func main() {
	var a int64 = 100
	reflectSetValue(&a)
	fmt.Println(a)

	var b string = "你好go"
	reflectSetValue(&b)
	fmt.Println(b)
}

结构体反射

与结构体相关的方法

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)NumField()Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

Go,冲了_第17张图片

StructField类型

type StructField struct {
	//参见http://golang.org/ref/spec#Uniqueness_of_identifiers
	Name string // Name是字段的名字
	PkgPath string	//PkgPath是非导出字段的包路径,对导出字段该字段为”
	Type	Type	//字段的类型
	Tag	StructTag //字段的标签
	Offset	uintptr	//字段在结构体中的字节偏移量
	Index	[]int	//用于Type.FieldByIndex时的索引切片
	Anonymous bool	//是否匿名字段
}

如何获取属性

//定义结构体
//Student结构体
type Student struct {
	Name  string `json:"name1" form:"username"`
	Age   int    `json:"age"`
	Score int    `json:"score"`
}

func (s Student) GetInfo() string {
	str := fmt.Sprintf("姓名:%v,年龄:%v,成绩:%v", s.Name, s.Age, s.Score)
	return str
}
func (s *Student) SetInfo(name string, age, score int) {
	s.Name = name
	s.Age = age
	s.Score = score
}
func (s Student) print() {
	fmt.Println("这就是个打印方法....")
}
//打印字段
func PrintStructField(s interface{}) {
	//判断参数是不是结构体类型
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)
	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
		fmt.Println("传入的参数不是一个结构体")
		return
	}

	//通过类型变量里面的Field可以获取结构体的字段
	field0 := t.Field(0)                           
	fmt.Printf("%#v \n", field0)                   
	fmt.Println("字段名称: ", field0.Name)             //字段名称:  Name
	fmt.Println("字段类型: ", field0.Type)             //字段类型:  string
	fmt.Println("字段tag: ", field0.Tag.Get("json")) //字段tag:  name1
	fmt.Println("字段tag: ", field0.Tag.Get("form")) //字段tag:  username
	fmt.Println("=============")
	//通过类型变量里面的FieldByName可以获取结构体的字段
	field1, ok := t.FieldByName("Age")
	if ok {
		fmt.Println("字段名称: ", field1.Name)
		fmt.Println("字段类型: ", field1.Type)
		fmt.Println("字段tag: ", field1.Tag.Get("json"))
	}
	fmt.Println("=============")
	//通过类型变量里面的NumField获取到该结构体有几个字段
	var fieldCount = t.NumField()
	fmt.Println("结构体有", fieldCount, "个属性")	//结构体有 3 个属性
	fmt.Println("=============")
	//通过值变量获取结构体属性对应的值
	fmt.Println(v.FieldByName("Name"))
	fmt.Println(v.FieldByName("Age"))
	fmt.Println("=============")
	//通过for循环获取所有的属性
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		value := v.Field(i)
		fmt.Printf("属性名称:%v,属性值:%v,属性类型:%v,属性Tag:%v\n",field.Name,value,field.Type,field.Tag.Get("json"))
	}
}
func main() {
	s1 := Student{
		Name:  "花玲",
		Age:   18,
		Score: 100,
	}
	PrintStructField(s1)
}

字段

//打印字段
func PrintStructField(s interface{}) {
	//判断参数是不是结构体类型
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)
	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
		fmt.Println("传入的参数不是一个结构体")
		return
	}

	//通过类型变量里面的Field可以获取结构体的字段
	field0 := t.Field(0)                           //获取第0个属性,此时是Name
	fmt.Printf("%#v \n", field0)                   //reflect.StructField{Name:"Name", PkgPath:"", Type:(*reflect.rtype)(0x4aef40), Tag:"json:\"name\"", Offset:0x0, Index:[]int{0}, Anonymous:false}
	fmt.Println("字段名称: ", field0.Name)             //字段名称:  Name
	fmt.Println("字段类型: ", field0.Type)             //字段类型:  string
	fmt.Println("字段tag: ", field0.Tag.Get("json")) //字段tag:  name1
	fmt.Println("字段tag: ", field0.Tag.Get("form")) //字段tag:  username
	fmt.Println("=============")
	//通过类型变量里面的FieldByName可以获取结构体的字段
	field1, ok := t.FieldByName("Age")
	if ok {
		fmt.Println("字段名称: ", field1.Name)
		fmt.Println("字段类型: ", field1.Type)
		fmt.Println("字段tag: ", field1.Tag.Get("json"))
	}
	fmt.Println("=============")
	//通过类型变量里面的NumField获取到该结构体有几个字段
	var fieldCount = t.NumField()
	fmt.Println("结构体有", fieldCount, "个属性") //结构体有 3 个属性
	fmt.Println("=============")
	//通过值变量获取结构体属性对应的值
	fmt.Println(v.FieldByName("Name"))
	fmt.Println(v.FieldByName("Age"))
	fmt.Println("=============")
	//通过for循环获取所有的属性
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		value := v.Field(i)
		fmt.Printf("属性名称:%v,属性值:%v,属性类型:%v,属性Tag:%v\n", field.Name, value, field.Type, field.Tag.Get("json"))
	}
}

执行方法

func PrintStructFn(s interface{}) {
	//判断参数是不是结构体类型
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)
	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
		fmt.Println("传入的参数不是一个结构体")
		return
	}

	//通过类型变量里面的Method以获取结构体的方法
	method0 := t.Method(0)    //这个0和方法的顺序无关,是根据ASCII码有关
	fmt.Println(method0.Name) //GetInfo
	fmt.Println(method0.Type) //func(main.Student) string
	fmt.Println("-------------------")

	//通过类型变量获取这个结构体有多少个方法
	fmt.Println("方法个数:", t.NumMethod())
	method1, ok := t.MethodByName("Print")
	if ok {
		fmt.Println(method1.Name) //Print
		fmt.Println(method1.Type) //func(main.Student)
	}
	fmt.Println("-------------------")

	//通过<值变量>执行方法(注意 需要使用值变量,并且要注意参数) v.Method(0).Call(nil)或者v.MethodBy
	v.Method(1).Call(nil) //这就是个打印方法....

	//执行方法传入参数(注意需要使用<值变量>,并且要注意参数,接收的参数是[]reflect.Value的切片)
	fmt.Println(v.MethodByName("GetInfo").Call(nil)) //[姓名:花玲,年龄:18,成绩:100]
	var params = []reflect.Value{
		reflect.ValueOf("kk"),
		reflect.ValueOf(18),
		reflect.ValueOf(100),
	}
	v.MethodByName("SetInfo").Call(params)
	fmt.Println(v.MethodByName("GetInfo").Call(nil)) //[姓名:kk,年龄:18,成绩:100]

}

通过反射修改结构体属性

func reflectChangeStruct(s interface{}) {
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)
	if t.Kind() != reflect.Ptr {
		fmt.Println("传入的不是指针类型")
		return
	} else if t.Elem().Kind() != reflect.Struct {
		fmt.Println("传入的不是结构体指针类型")
		return
	}

	//修改结构体属性的值
	name :=v.Elem().FieldByName("Name")
	name.SetString("kk")

	age := v.Elem().FieldByName("Age")
	age.SetInt(21)
}

主函数

func main() {
	s1 := Student{
		Name:  "花玲",
		Age:   18,
		Score: 100,
	}
	//PrintStructField(s1)
	//PrintStructFn(&s1)
	reflectChangeStruct(&s1)
	fmt.Printf("%#v\n",s1)	//main.Student{Name:"kk", Age:21, Score:100}
}

不要乱用反射!

反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。

  1. 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
  2. 大量使用反射的代码通常难以理解。

文件 目录操作

读取文件

只读方式打开文件file,err := os .Open()

func main() {
	//只读方式打开文件
	file, err := os.Open("p:/a.txt")
	defer file.Close()	//必须关闭
	if err != nil {
		fmt.Println(err)
		return
	}
	//读取文件内容
	fileSlice := make([]byte,128)	//每次读取128字节
	var strSlice []byte
	for  {
		n, err := file.Read(fileSlice)
		if err == io.EOF {	//err==io.EOF表示读取完毕
			fmt.Println("读取完毕")
			break
		}
		if err != nil {
			fmt.Println("读取失败")
			return
		}
		fmt.Printf("读取到了%v个字节\n",n)
		strSlice = append(strSlice, fileSlice[:n]...)	//最后可能读取到的块没有128字节了,为了避免切片填充,限制切片范围 
	}

	fmt.Println(string(strSlice))	//读出来的是byte切片,我们需要转换成string再来打印

}

读取文件(方法2) bufio读取文件

java开发的同学应该不难看出这个很像是字节流

func main() {
	file, err := os.Open("p:/a.txt")
	defer file.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	//读取文件
	reader := bufio.NewReader(file)
	var out string
	for  {
		str, err := reader.ReadString('\n')
		if err == io.EOF {
			fmt.Println("读取结束")
			out+=str	//这里也需要拼接,不然会读不全
			break
		}
		if err != nil {
			fmt.Println(err)
			return
		}
		out+=str
	}

	fmt.Println(out)
}

读取文件(方法3) ioutil读取文件

最简单方式,如果文件比较小,可以用这种方式来读取

func main() {
	fileByte, err := ioutil.ReadFile("p:/a.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(fileByte))
}

写入文件

方法1 os.OpenFile()

模式 含义
os.O_ WRONLY 只写
os.O_ CREATE 创建文件
os.O_ RDONLY 只读
os.O_ RDWR 读写
os.O_ TRUNC 清空
os.O_ APPEND 追加

perm:文件权限,一个八进制数。r(读) 04,w (写) 02,x (执行) 01。一般来说传0666就行

os.OpenFile()需要传入三个参数,文件路径,模式,文件权限

func main() {
	file, err := os.OpenFile("p:/a.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
	defer file.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	//写入文件
	for i := 0; i < 10; i++ {
		file.WriteString(strconv.Itoa(i)+"花玲花玲花玲花玲花玲花玲\r\n")
	}
    
    var str = "直接写入的字符串数据byte"
	file.Write([]byte(str))	//这样子也可以
}

写入文件(方法2) bufio 写入文件

记得flush,java开发的同学应该都知道

func main() {
	file, err := os.OpenFile("p:/a.txt", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
	defer file.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	writer := bufio.NewWriter(file)
	for i := 0; i < 10; i++ {
		writer.WriteString("你好\r\n") //将数据先写入缓存
	}
	writer.Flush() //将缓存中的内容写入文件
}

写入文件(方法3)ioutil写入文件

但是这个,没有append模式,测试写东西的话可以用一下,写日志还是用上面的吧

func main() {
	str := "你好offer"
	err := ioutil.WriteFile("p:/a.txt",[]byte(str),0666)
	if err != nil {
		fmt.Println(err)
		return
	}
}

目录操作

复制文件 ioutil

func copyFile(srcFileName, dstFileName string) (err error) {
	bytestr, err := ioutil.ReadFile(srcFileName)
	if err != nil {
		return err
	}
	err1 := ioutil.WriteFile(dstFileName, bytestr, 006)
	if err1 != nil {
		return err1
	}
	return nil
}

func main() {
	src := "p:/a.txt"
	dst := "p:/b.txt"
	err := copyFile(src, dst)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("复制成功")
}

复制文件 文件流

func copyFile(srcFileName, dstFileName string) (err error) {
	sFile, err1 := os.Open(srcFileName)
	defer sFile.Close()	//不关闭可能造成内存泄漏
	dFile, err2 := os.OpenFile(dstFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	defer dFile.Close()
	if err1 != nil {
		return err1
	}
	if err2 != nil {
		return err2
	}
	var fileSlice = make([]byte, 1024*10)
	for true {
		//读取数据
		n1, err := sFile.Read(fileSlice)
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		//写入数据
		if _, err4 := dFile.Write(fileSlice[:n1]); err4 != nil {
			return err4
		}

	}
	return nil
}

func main() {
	src := "p:/a.txt"
	dst := "p:/b.txt"
	err := copyFile(src, dst)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("复制成功")
}

创建目录

func main() {
	os.Mkdir("p:/a", 0666)	//创建单个目录
    
	os.MkdirAll("p:/a/b/c",0666)	//创建多级目录
}

删除文件

func main() {
	err := os.Remove("p:/aaa.txt") //可以删除一个文件也可以删除文件夹
	if err != nil {
		fmt.Println(err)
	}

	err1 := os.RemoveAll("p:/a.txt") //级联删除
	if err1!=nil {
		fmt.Println(err1)
	}
}

重命名

func main() {
	err := os.Rename("p:/a.txt", "p:/b.txt")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("重命名成功")
}

你可能感兴趣的:(Go,冲了)