• Go结构体&接口&反射


    Go结构体&接口&并发&反射

    一、结构体struct

    0、Type关键字

    Golang中通过type关键词定义一个结构体,需要注意的是,数组和结构体都是值类型

    Go语言中可以使用type关键字来定义自定义类型:

    type myInt int//通过type关键字的定义,mylnt就是一种新的类型,它具有int的特性
    
    • 1

    类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型

    type TypeAlias = Type
    
    • 1

    rune 和 byte 就是类型别名,他们的底层代码如下:

    type byte = uint8
    type rune = int32
    
    • 1
    • 2

    1、struct定义及使用

    Go通过结构体struct和interface实现oop(面向对象编程)

    struct的成员(也叫属性或字段)可以是任何类型,如普通类型、复合类型、函数、 map、 interface、
    struct等

    使用type 和 struct关键字来定义结构体:

    type struct_variable_type struct {
        member member_type
        member member_type
        .....
        member member_type
    }
    type Student struct {
        name string
        age int //小写私有成员(对外不可见)
        Class string //首字母大写则该成员为公有成员(对外可见)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    声明与初始化:

    var stu1 Student
    stu1.name="zhangsan"
    //...
    var stu2 *Student= &Student{name:"zhangsan",age:10,Class:"1"} //返回指针,等同于new实例化操作
    var stu3 = Student{name:"zhangsan",age:10,Class:"1"} 
    var stu4 = new(Student)
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    自定义‘构造函数’方法:

    func Newstu(name1 string,age1 int,class1 string) *Student {
        return &Student{name:name1,age:age1,Class:class1}
    }
    func main() {
        stu1 := Newstu("zhangsan"",34,"math")
        fmt.Println(stu1.name)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注:go中的结构体内存布局和c结构体布局类似,每个成员的内存分布是连续的,结构体大小遵循对齐原则

    2、struct tag

    tag可以为结构体的成员添加说明或者标签便于使用,这些说明可以通过反射获取到

    此外还可以方便进行json序列化操纵

    type Student struct {
    	ID     string `json:"id1"`
    	Gender string `json:"gender"`
    	Name   string `json:"name"`
    	Sno    string `json:"son"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    获取tag信息:

    var s1 = Student{}
    	st := reflect.TypeOf(s1)
    	field1 := st.Field(0)
    	fmt.Println("key1:", field1.Tag.Get("json"))
    
    • 1
    • 2
    • 3
    • 4

    json序列化操作:

    // Json字符串转换成结构体
    var str = `{"id1":"12","name":"李四","gender":"男","sno":"s001"}`
    var s2 = Student{}
    err := json.Unmarshal([]byte(str), &s2)
    if err != nil {
        fmt.Printf("转换失败 \n")
    } else {
        fmt.Printf("%#v \n", s2)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注:要实现结构体转换成字符串,必须保证结构体中的字段是公有的,也就是首字母必须是大写的,这样才能够实现结构体 到 Json字符串的转换。

    3、struct匿名成员

    结构体中,每个成员不一定都有名称,也允许字段没有名字,即匿名成员

    匿名成员的一个重要作用,可以用来实现oop中的继承

    同一种类型匿名成员只允许最多存在一个

    当匿名成员是结构体时,且两个结构体中都存在相同字段时,优先选择最近的字段

    type Person struct {
        Name string
        Age int
    }
    type Student struct {
        score string
        Age int
        Person // 匿名内嵌结构体
    }
    func main() {
        var stu = new(Student)
        stu.Age = 34 //优先选择Student中的Age
        fmt.Println(stu.Person.Age, stu.Age) // 0,34
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3、struct继承

    当结构体中的成员也是结构体时, 该结构体就继承了这个结构体,继承了其所有的方法与属性,当然有多个结构体成员也就是多继承

    访问父结构中属性也使用“.”,但是当子结构体中存在和父结构中的字段相同时候,只能使用: "子结构体.父结构体.字段"访问父结构体中的属性,如上面示例的stu.Person.Age

    继承结构体可以使用别名,访问的时候通过别名访问

    type Person struct {
    	Name string
    	Age  int
    }
    type Teacher struct {
    	Salary  int
    	Classes string
    }
    type man struct {
    	sex    string
    	job    Teacher //别名,继承Teacher 这个时候就不是匿名了
    	Person         //继承Person
    }
    func main() {
    	var man1 = new(man)
    	man1.sex = "man"
    	man1.Age = 34
    	man1.Name = "zhangsan"
    	man1.job.Classes = "111"
    	man1.job.Salary = 100000
    	fmt.Println(man1, man1.job.Salary)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    4、struct方法

    Go方法是作用在接受者上的一个函数,接受者是某种类型的变量,因此方法是一种特殊类型的函数。

    接收者可以是任何类型,不仅仅是结构体, Go中的基本类型(int, string, bool等)也是可以,或者说数组的别名类型,甚至可以是函数类型。但是, 接受者不能是一个接口类型,因为接口是一个抽象的定义,方法是一个具体实现。

    一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件, 唯一的要求是:它们必须是同一个包的。

    类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。

    因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的

    别名类型不能有它原始类型上已经定义过的方法(因为别名类型和原始类型底层是一样的)。

    定义方法的格式

    func(recv recevier_type(结构体))methodName(parameter_list)(return _value_list){}  
    
    • 1

    结构体的指针方法

    接收者是指针时,方法可以改变接受者的值(或状态),函数也能做到

    当接受者是一个值的时候,这个值是该类型实例的拷贝;如果想要方法改变接受者的数据,就在接受者的指针类型上定义该方法

    二、接口interface

    1、接口定义和使用

    interface(接口)是golang最重要的特性之一, Interface类型可以定义一组方法,但是这些不需要实
    现,并且interface不能包含任何变量

    interface 是方法的集合,interface是一种类型,并且是指针类型,interface的 更重要的作用在于多态实现

    接口定义:

    type 接口名称 interface {
        method1 (参数列表) 返回值列表
        method2 (参数列表) 返回值列表
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注:

    • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等,接口名最好突出该接口的类型含义。
    • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
    • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名是可以省略

    接口的使用不仅仅针对结构体,自定义类型、变量等等都可以实现接口

    要实现一个接口/使用接口对象, 接口接收的对象类型就必须实现该接口里面的所有方法

    //定义接口
    type Skills interface {
        Running()
        Getname() string
    }
    type Student struct {
        Name string
        Age int
    }
    // 实现接口
    func (p Student) Getname() string { //实现Getname方法
        fmt.Println(p.Name)
        return p.Name
    }
    func (p Student) Running() { // 实现 Running方法
        fmt.Printf("%s running", p.Name)
    }
    // 使用接口
    func main() {
        var skill Skills
        var stu1 Student
        stu1.Name = "zhangsan"
        stu1.Age = 34
        skill = stu1
        skill.Running() //调用接口
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2、空接口

    如果一个接口没有任何方法,我们称为空接口,由于空接口没有方法, 任意结构体都隐式地实现了空接口

    // 空接口表示没有任何约束,任意的类型都可以实现空接口
    type EmptyA interface {
    }
    func main() {
    	var a EmptyA
    	var str = "你好golang"
    	// 让字符串实现A接口
    	a = str
    	fmt.Println(a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    golang中空接口也可以直接当做类型来使用,可以表示任意类型。相当于Java中的Object类型:

    var a interface{}
    a = 20
    a = "hello"
    a = true
    
    • 1
    • 2
    • 3
    • 4

    空接口可以作为函数的参数,使用空接口可以接收任意类型的函数参数:

    // 空接口作为函数参数
    func show(a interface{}) {
        fmt.println(a)
    }
    
    • 1
    • 2
    • 3
    • 4

    接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

    如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:

    x.(T)//X:表示类型为interface{}的变量  T:表示断言x可能是的类型
    
    • 1
    // 类型断言
    var a interface{}
    a = "132"
    value, isString := a.(string)
    if isString {
        fmt.Println("是String类型, 值为:", value)
    } else {
        fmt.Println("断言失败")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用switch语句来实现类型判断

    func Print2(x interface{})  {
    	switch x.(type) {
    	case int:
    		fmt.Println("int类型")
    	case string:
    		fmt.Println("string类型")
    	case bool:
    		fmt.Println("bool类型")
    	default:
    		fmt.Println("其它类型")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    空接口如果值类型为切片,无法直接通过索引获取数组中的内容,只能使用类型断言:

    // 这个时候我们就可以使用类型断言了
    hobbyValue,ok := userInfo["hobby"].([]string)
    if ok {
        fmt.Println(hobbyValue[0])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3、接口多态及嵌套

    go语言中interface是实现多态的一种形式,所谓多态, 就是一种事物的多种形态

    同一个interface,不同的类型实现,都可以通过接口进行统一调用,也就是多态

    func main() {
    	var skill Skills
    	var stu1 Student
    	var t1 Teacher
    	t1.Name = "zhangsan"
    	stu1.Name = "lisi"
    	stu1.Age = 22
    	skill = stu1
    	skill.Running()
    	skill = t1
    	t1.Running()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    go语言中的接口可以嵌套,可以理解为继承,子接口拥有父接口的所有方法

    如果使用该子接口,必须将父接口和子接口的所有方法都实现

    接口的定义也支持组合嵌套(多嵌套)

    type Skills interface {
        Running()
        Getname() string
    }
    type Test interface {
        sleeping()
        Skills //继承Skills
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    三、Go并发

    1、go协程

    golang中的主线程:(可以理解为线程/也可以理解为进程),在一个Golang程

    序的主线程上可以起多个协程。Golang中多协程可以实现并行或者并发。

    多协程和多线程:Golang中每个goroutine(协程)默认占用内存远比Java、C的线程少。

    OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB左右),一个goroutine(协程)占用内存非常小,只有2KB左右,多协程goroutine切换调度开销方面远比线程要少。

    协程

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

    // 协程需要运行的方法
    func test()  {
    	for i := 0; i < 5; i++ {
    		fmt.Println("test 你好golang")
    		time.Sleep(time.Millisecond * 100)
    	}
    }
    func main() {
    	// 通过go关键字,就可以直接开启一个协程
    	go test()
    	// 这是主进程执行的
    	for i := 0; i < 5; i++ {
    		fmt.Println("main 你好golang")
    		time.Sleep(time.Millisecond * 100)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上述的代码其实还有问题的,也就是说当主进程执行完毕后,不管协程有没有执行完成,都会退出,需要用到 sync.WaitGroup等待协程

    协程计数器使用:

    // 定义一个协程计数器
    var wg sync.WaitGroup
    // 开启协程,协程计数器加1
    wg.Add(1)
    // 协程计数器减1
    wg.Done()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实现代码:

    // 定义一个协程计数器
    var wg sync.WaitGroup
    func test()  {
    	// 这是主进程执行的
    	for i := 0; i < 1000; i++ {
    		fmt.Println("test1 你好golang", i)
    		//time.Sleep(time.Millisecond * 100)
    	}
    	// 协程计数器减1
    	wg.Done()
    }
    func test2()  {
    	// 这是主进程执行的
    	for i := 0; i < 1000; i++ {
    		fmt.Println("test2 你好golang", i)
    		//time.Sleep(time.Millisecond * 100)
    	}
    	// 协程计数器减1
    	wg.Done()
    }
    func main() {
    	// 通过go关键字,就可以直接开启一个协程
    	wg.Add(1)
    	go test()
    	// 协程计数器加1
    	wg.Add(1)
    	go test2()
    	// 这是主进程执行的
    	for i := 0; i < 1000; i++ {
    		fmt.Println("main 你好golang", i)
    		//time.Sleep(time.Millisecond * 100)
    	}
    	// 等待所有的协程执行完毕
    	wg.Wait()
    	fmt.Println("主线程退出")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    func main() {
    	// 获取cpu个数
    	npmCpu := runtime.NumCPU()
    	fmt.Println("cup的个数:", npmCpu)
    	// 设置允许使用的CPU数量
    	runtime.GOMAXPROCS(runtime.NumCPU() - 1)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

    2、chan管道

    管道是Golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。

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

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

    channel是一种引用类型,使用如下:

    // 声明一个传递整型的管道
    var ch1 chan int
    // 声明一个传递int切片的管道
    var ch3 chan []int
    // 创建一个能存储10个int类型的数据管道
    ch1 = make(chan int, 10)
    // 创建一个能存储3个[]int切片类型的管道
    ch3 = make(chan []int, 3)
    // 把10发送到ch中
    ch <- 10
    // 从管道ch中获取值
    x := <- ch
    //关闭管道资源
    close(ch)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    // 创建管道
    ch := make(chan int, 3)
    // 给管道里面存储数据
    ch <- 10
    ch <- 21
    ch <- 32
    // 获取管道里面的内容
    a := <- ch
    fmt.Println("打印出管道的值:", a)
    fmt.Println("打印出管道的值:", <- ch)
    fmt.Println("打印出管道的值:", <- ch)
    // 管道的值、容量、长度
    fmt.Printf("地址:%v 容量:%v 长度:%v \n", ch, cap(ch), len(ch))
    // 管道的类型
    fmt.Printf("%T \n", ch)
    // 管道阻塞(当没有数据的时候取,会出现阻塞,同时当管道满了,继续存也会)
    <- ch  // 没有数据取,出现阻塞
    ch <- 10
    ch <- 10
    ch <- 10
    ch <- 10 // 管道满了,继续存,也出现阻塞
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    for range从管道循环取值:

    当管道被关闭时,再往该管道发送值会引发panic;当管道被关闭时,从管道中取值,会一直取,直到没有返回零值

    // for range循环遍历管道的值(管道没有key)
    for value := range ch {
        fmt.Println(value)
    }
    
    • 1
    • 2
    • 3
    • 4

    注:使用for range遍历的时候,一定在之前需要先关闭管道,否则会发生死锁,for i的循环方式,可以不关闭管道

    size := len(ch)
    for i := 0; i < size; i++ {
        fmt.Println(<-ch)
    }
    
    • 1
    • 2
    • 3
    • 4

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

    func write(ch chan int)  {
    	for i := 0; i < 10; i++ {
    		fmt.Println("写入:", i)
    		ch <- i
    		time.Sleep(time.Microsecond * 10)
    	}
    	wg.Done()
    }
    func read(ch chan int)  {
    	for i := 0; i < 10; i++ {
    		fmt.Println("读取:", <- ch)
    		time.Sleep(time.Microsecond * 10)
    	}
    	wg.Done()
    }
    var wg sync.WaitGroup
    func main() {
    	ch := make(chan int, 10)
    	wg.Add(1)
    	go write(ch)
    	wg.Add(1)
    	go read(ch)
    	// 等待
    	wg.Wait()
    	fmt.Println("主线程执行完毕")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    注:管道是安全的,是一边写入,一边读取,当读取比较快的时候,会等待写入

    案例2:goroutine 结合 channel打印素数

    
    // 想intChan中放入 1~ 120000个数
    func putNum(intChan chan int) {
    	for i := 2; i < 120000; i++ {
    		intChan <- i
    	}
    	wg.Done()
    	close(intChan)
    }
    // cong intChan取出数据,并判断是否为素数,如果是的话,就把得到的素数放到primeChan中
    func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    	for value := range intChan {
    		for i := 2; i <= int(math.Sqrt(float64(value))); i++ {
    			if i%i == 0 {
    				continue
    			}
    		}
    		primeChan <- value
    	}
    	exitChan <- true
    	wg.Done()
    }
    // 打印素数
    func printPrime(primeChan chan int) {
    	for value := range primeChan {
    		fmt.Println(value)
    	}
    	wg.Done()
    }
    
    var wg sync.WaitGroup
    func main() {
    	// 写入数字
    	intChan := make(chan int, 1000)
    	// 存放素数
    	primeChan := make(chan int, 1000)
    	// 存放 primeChan退出状态
    	exitChan := make(chan bool, 16)
    	// 开启写值的协程
    	go putNum(intChan)
    	// 开启计算素数的协程
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go primeNum(intChan, primeChan, exitChan)
    	}
    	// 开启打印的协程
    	wg.Add(1)
    	go printPrime(primeChan)
    	// 匿名自运行函数
    	wg.Add(1)
    	go func() {
    		for i := 0; i < 10; i++ {
    			// 如果exitChan 没有完成10次遍历,将会等待
    			<-exitChan
    		}
    		// 关闭primeChan
    		close(primeChan)
    		wg.Done()
    	}()
    	wg.Wait()
    	fmt.Println("主线程执行完毕")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    3、单向管道

    有时候管道会作为参数在多个任务函数间传递,在不同的任务函数中,使用管道都会对其进行限制,比如限制管道在函数中只能发送或者只能接受,而默认的管道是 可读可写

    // 定义一种可读可写的管道
    var ch = make(chan int, 2)
    ch <- 10
    <- ch
    // 管道声明为只写管道,只能够写入,不能读
    var ch2 = make(chan<- int, 2)
    ch2 <- 10
    // 声明一个只读管道
    var ch3 = make(<-chan int, 2)
    <- ch3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4、Select多路复用

    在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到golang中给我们提供的select多路复用,,可以同时响应多个管道的操作。

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

    intChan := make(chan int, 10)
    intChan <- 10
    intChan <- 12
    intChan <- 13
    stringChan := make(chan int, 10)
    stringChan <- 20
    stringChan <- 23
    stringChan <- 24
    
    // 每次循环的时候,会随机中一个chan中读取,其中for是死循环
    for {
        select {
            case v:= <- intChan:
            fmt.Println("从initChan中读取数据:", v)
            case v:= <- stringChan:
            fmt.Println("从stringChan中读取数据:", v)
            default:
            fmt.Println("所有的数据获取完毕")
            return
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    注:使用select来获取数据的时候,不需要关闭chan,不然会出现问题

    go协程发生panic捕获异常:

    // 捕获异常
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("errTest发生错误")
        }
    }()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5、协程互斥

    互斥锁:

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

    // 定义一个锁
    var mutex sync.Mutex
    // 协程访问共享资源前,先获取锁资源,进行加锁
    mutex.Lock()
    // 访问完后释放锁资源
    mutex.Unlock()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    读写互斥锁:

    互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

    其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。

    所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

    因此,衍生出另外一种锁,叫做读写锁。

    读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

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

    四、反射

    1、反射

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

    空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?

    • 可以使用类型断言
    • 可以使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息。

    把结构体序列化成json字符串,自定义结构体Tab标签的时候就用到了反射

    后面所说的ORM框架,底层就是用到了反射技术

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

    反射介绍:

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

    反射的功能:

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

    变量是分为两部分的:

    • 类型信息:预先定义好的元信息。
    • 值信息:程序运行过程中可动态变化的。

    反射使用:

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

    使用reflect.TypeOf()函数可以接受任意参数,可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息

    v := reflect.TypeOf(x)
    fmt.Println(v)
    
    • 1
    • 2

    反射修改变量的值:

    func main() {
        i := 1
        v := reflect.ValueOf(&i)//传入地址才能修改变量
        v.Elem().SetInt(10)//Elem()获取指针指向的变量 SetInt()修改值
        fmt.Println(i)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、Name和Kind

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

    v := reflect.TypeOf(x)
    fmt.Println("类型 ", v)
    fmt.Println("类型名称 ", v.Name())
    fmt.Println("类型种类 ", v.Kind())
    
    • 1
    • 2
    • 3
    • 4

    反射和switch:

    // 通过反射来获取变量的原始值
    v := reflect.ValueOf(x)
    // 获取种类
    kind := v.Kind()
    switch kind {
        case reflect.Int:
        fmt.Println("我是int类型")
        case reflect.Float64:
        fmt.Println("我是float64类型")
        default:
        fmt.Println("我是其它类型")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    方法说明
    interface{}将值以interface{}类型返回,可以通过类型断言转换为指定类型
    Int() int64将值以int类型返回,所有有符号整型均可以此方式返回
    Uint() uint64将值以uint类型返回,所有无符号整型均可以以此方式返回
    Float() float64将值以双精度(float 64)类型返回,所有浮点数(float 32、float64)均可以以此方式返回

    3、结构体反射

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

    获取结构体成员相关的的方法:

    方法说明
    Field(i int)StructField根据索引,返回索引对应的结构体字段的信息
    NumField() int返回结构体成员字段数量
    FieldByName(name string)(StructField, bool)根据给定字符串返回字符串赌赢的结构体字段信息
    FieldByIndex(index []int)StructField多层成员访问时,根据[] int 提供的每个结构
    type Student4 struct {
    	Name  string `json: "name"`
    	Age   int    `json: "age"`
    	Score int    `json: "score"`
    }
    func (s Student4) GetInfo() string {
    	var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v", s.Name, s.Age, s.Score)
    	return str
    }
    func (s *Student4) SetInfo(name string, age int, score int) {
    	s.Name = name
    	s.Age = age
    	s.Score = score
    }
    func (s Student4) PrintStudent() {
    	fmt.Println("打印学生: name#", s.Name, " age#", s.Age, " score#", s.Score)
    }
    func PrintStructFn(s interface{}) {
    	t := reflect.TypeOf(s)
    	// 判断传递过来的是否是结构体
    	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
    		fmt.Println("请传入结构体类型!")
    		return
    	}
    	// 通过类型变量里面的Method,可以获取结构体的方法
    	method0 := t.Method(0)
    	// 获取第一个方法, 这个是和ACSII相关
    	fmt.Println(method0.Name)
    	// 通过类型变量获取这个结构体有多少方法
    	methodCount := t.NumMethod()
    	fmt.Println("拥有的方法", methodCount)
    	// 通过值变量 执行方法(注意需要使用值变量,并且要注意参数)
    	v := reflect.ValueOf(s)
    	// 通过值变量来获取参数
    	v.MethodByName("PrintStudent").Call(nil)
    	// 手动传参
    	var params = make([]reflect.Value, 3)
    	params[0] = reflect.ValueOf("张三")
    	params[1] = reflect.ValueOf(23)
    	params[2] = reflect.ValueOf(99)
    	v.MethodByName("SetInfo").Call(params)
    	v.MethodByName("PrintStudent").Call(nil)
    }
    func main() {
    	s := Student4{}
    	PrintStructFn(&s)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    image-20230721001749578

    注:只有公有函数(首字母大写的函数)可以通过reflect.MethodByName()函数获取,私有方法是不行的。

    4、反射的优劣

    反射的好处:

    1. 为了降低多写代码造成的bug率,做更好的归约和抽象

    2. 为了灵活、好用、方便,做动态解析、调用和处理

    3. 为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别

    反射的弊端:

    1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。

    2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。

    3. 反射对性能影响还是比较大的, 比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性

    rintln(“拥有的方法”, methodCount)
    // 通过值变量 执行方法(注意需要使用值变量,并且要注意参数)
    v := reflect.ValueOf(s)
    // 通过值变量来获取参数
    v.MethodByName(“PrintStudent”).Call(nil)
    // 手动传参
    var params = make([]reflect.Value, 3)
    params[0] = reflect.ValueOf(“张三”)
    params[1] = reflect.ValueOf(23)
    params[2] = reflect.ValueOf(99)
    v.MethodByName(“SetInfo”).Call(params)
    v.MethodByName(“PrintStudent”).Call(nil)
    }
    func main() {
    s := Student4{}
    PrintStructFn(&s)
    }

    
    [外链图片转存中...(img-lCRUAbuc-1698414788807)]
    
    注:只有公有函数(首字母大写的函数)可以通过reflect.MethodByName()函数获取,私有方法是不行的。
    
    ### 反射的优劣
    
    反射的好处:
    
    1. 为了降低多写代码造成的bug率,做更好的归约和抽象
    
    2. 为了灵活、好用、方便,做动态解析、调用和处理
    
    3. 为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别
    
    反射的弊端:
    
    1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
    
    2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
    
    3. 反射对性能影响还是比较大的, 比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性  
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  • 相关阅读:
    云计算,用价格让利换创新空间?
    校园跑腿小程序--表白墙--仿造微信朋友圈
    java 处理日期的方法
    详解C语言指针(二)
    python测试工程师 之 python基础大全【13万字总结】
    如何回复SCI审稿人评审意见(response letter)
    图机器学习(GML)&图神经网络(GNN)原理和代码实现(前置学习系列二)
    JavaWeb三大组件以及Tomcat工作机制
    Pinsker 不等式证明(Proof of Pinsker‘s Inequality)
    怎么把Mesh转为SDF
  • 原文地址:https://blog.csdn.net/CS_z_jun/article/details/134084676