Go 语言入门指南:基础语法和常用特性解析|青训营

Go学习笔记

一、基础语法

1、变量声明

变量声明

第一种,指定变量类型,声明后若不赋值,使用默认值0。

var v_name v_type
v_name = value
package main















import "fmt"













func main() {
        var a int
        fmt.Printf(" = %d\n", a)
}




第二种,根据值自行判定变量类型。

var v_name = value

第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误。

v_name := value




































// 例如
var a int = 10
var b = 10
c : = 10

多变量声明

package main




















































import "fmt"
































var x, y int
var ( //这种分解的写法,一般用于声明全局变量
        a int
        b bool
)












var c, d int = 1, 2
var e, f = 123, "liudanbing"








//这种不带声明格式的只能在函数体内声明
//g, h := 123, "需要在func函数体内实现"








func main() {
        g, h := 123, "需要在func函数体内实现"
        fmt.Println(x, y, a, b, c, d, e, f, g, h)







        //不能对g变量再次做初始化声明
        //g := 400








        _, value := 7, 5  //实际上7的赋值被废弃,变量 _  不具备读特性
        //fmt.Println(_) //_变量的是读不出来的
        fmt.Println(value) //5
}

2、常量

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

  • 显示定义

    const b string = "abc"
    
  • 隐式定义

    const b = "abc"
    

常量还可以用作枚举:

const (

    Unknown = 0
    Female = 1
    Male = 2
)

数字 0、1 和 2 分别代表未知性别、女性和男性。

常量可以用len(), cap(), unsafe.Sizeof()常量计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:

package main




















































import "unsafe"
const (
    a = "abc"
    b = len(a)
    c = unsafe.Sizeof(a)
)














func main(){
    println(a, b, c)
}





输出结果为:abc, 3, 16

unsafe.Sizeof(a)输出的结果是16 。

字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。

常量 iota

自增长

在 golang 中,一个方便的习惯就是使用iota标示符,它简化了常量用于增长数字的定义,给以上相同的值以准确的分类。

const (

    CategoryBooks = iota // 0
    CategoryHealth       // 1
    CategoryClothing     // 2
)

iota和表达式

iota可以做更多事情,而不仅仅是 increment。更精确地说,iota总是用于 increment,但是它可以用于表达式,在常量中的存储结果值。

type Allergen int




































const (
    IgEggs Allergen = 1 << iota         // 1 << 0 which is 00000001
    IgChocolate                         // 1 << 1 which is 00000010
    IgNuts                              // 1 << 2 which is 00000100
    IgStrawberries                      // 1 << 3 which is 00001000
    IgShellfish                         // 1 << 4 which is 00010000
)

这个工作是因为当你在一个const组中仅仅有一个标示符在一行的时候,它将使用增长的iota取得前面的表达式并且再运用它,。在 Go 语言的spec中, 这就是所谓的隐性重复最后一个非空的表达式列表.

如果你对鸡蛋,巧克力和海鲜过敏,把这些 bits 翻转到 “on” 的位置(从左到右映射 bits)。然后你将得到一个 bit 值00010011,它对应十进制的 19。

fmt.Println(IgEggs | IgChocolate | IgShellfish)




































// output:
// 19
type ByteSize float64













const (
    _           = iota                   // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)       // 1 << (10*1)
    MB                                   // 1 << (10*2)
    GB                                   // 1 << (10*3)
    TB                                   // 1 << (10*4)
    PB                                   // 1 << (10*5)
    EB                                   // 1 << (10*6)
    ZB                                   // 1 << (10*7)
    YB                                   // 1 << (10*8)
)

3、函数

init函数与import

首先我们看一个例子:init函数:

init 函数可在package main中,可在其他package中,可在同一个package中出现多次。

main函数

main 函数只能在package main中。

执行顺序

golang里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。

包加载流程

init.png

指针

我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

以下我们通过使用引用传递来调用 swap() 函数:

package main




















































import "fmt"
































func main() {





   /* 定义局部变量 */
   var a int = 100
   var b int= 200











   fmt.Printf("交换前,a 的值 : %d\n", a )
   fmt.Printf("交换前,b 的值 : %d\n", b )











   /* 调用 swap() 函数
   * &a 指向 a 指针,a 变量的地址
   * &b 指向 b 指针,b 变量的地址
   */
   swap(&a, &b)











   fmt.Printf("交换后,a 的值 : %d\n", a )
   fmt.Printf("交换后,b 的值 : %d\n", b )
}




func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

4、defer

defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。

defer作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。

func Demo(){
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	defer fmt.Println("4")
}
func main() {





	Demo()
}

输出结果:

4
3
2
1

recover错误拦截

运行时panic异常一旦被引发就会导致程序崩溃。

Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。

**注意:**recover只有在defer调用的函数中有效。

package main





































import "fmt"



func Demo(i int) {
	//定义10个元素的数组
	var arr [10]int
	//错误拦截要在产生错误前设置
	defer func() {
		//设置recover拦截错误信息
		err := recover()
		//产生panic异常  打印错误信息
		if err != nil {
			fmt.Println(err)
		}
	}()
	//根据函数参数为数组元素赋值
	//如果i的值超过数组下标 会报错误:数组下标越界
	arr[i] = 10




}






func main() {
	Demo(10)
	//产生错误后 程序继续
	fmt.Println("程序继续执行...")
}

输出结果:

runtime error: index out of range
程序继续执行...

5、slice和map

slice

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片

可以通过声明一个未还定大小的数组来定义一个切片。

var identifier []type

切片不需要说明长度。

或使用make()函数来创建切片:

var slice1 []type = make([]type, len)




































也可以简写为
























slice1 := make([]type, len)

也可以指定容量,其中capacity为可选参数。

make([]T, length, capacity)

切片初始化

s :=[] int {1,2,3 }
//直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3















s := arr[:]
//初始化切片s,是数组arr的引用












s := arr[startIndex:endIndex]
//将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片





s := arr[startIndex:]
//缺省endIndex时将表示一直到arr的最后一个元素





s := arr[:endIndex]
//缺省startIndex时将表示从arr的第一个元素开始








s1 := s[startIndex:endIndex]
//通过切片s初始化切片s1




s :=make([]int,len,cap)
//通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片

len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

以下为具体实例:

package main




















































import "fmt"
































func main() {





   var numbers = make([]int,3,5)













   printSlice(numbers)

}















func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果为:

len=3 cap=5 slice=[0 0 0]

空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

package main




















































import "fmt"
































func main() {





   var numbers []int














   printSlice(numbers)













   if(numbers == nil){
      fmt.Printf("切片是空的")
   }
}










func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果为:

len=0 cap=0 slice=[]

切片是空的

切片截取

可以通过设置下限及上限来设置截取切片*[lower-bound:upper-bound]*,实例如下:

package main




















































import "fmt"
































func main() {





   /* 创建切片 */
   numbers := []int{0,1,2,3,4,5,6,7,8}   
   printSlice(numbers)






   /* 打印原始切片 */
   fmt.Println("numbers ==", numbers)




   /* 打印子切片从索引1(包含) 到索引4(不包含)*/
   fmt.Println("numbers[1:4] ==", numbers[1:4])





   /* 默认下限为 0*/
   fmt.Println("numbers[:3] ==", numbers[:3])




   /* 默认上限为 len(s)*/
   fmt.Println("numbers[4:] ==", numbers[4:])





   numbers1 := make([]int,0,5)
   printSlice(numbers1)



   /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
   number2 := numbers[:2]
   printSlice(number2)






   /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
   number3 := numbers[2:5]
   printSlice(number3)


}


func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果为:

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]

ps:在通过上下所以截取切片时,如果是从被截取切片的0号索引位置开始截取的,那么被赋值切片申请的初始cap与被截取的切片的cap相同,如果不是从被截取切片的 0号索引开始,则被赋值切片申请的cap会减去被舍弃片段的大小

append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

package main




















































import "fmt"
































func main() {





   var numbers []int

   printSlice(numbers)








   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)




   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)




   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)






   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)




   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)   
}







func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果为:

len=0 cap=0 slice=[]

len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

map

map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。

package main
















import (
    "fmt"
)












func main() {
    //第一种声明
    var test1 map[string]string
    //在使用map前,需要先make,make的作用就是给map分配数据空间
    test1 = make(map[string]string, 10) 
    test1["one"] = "php"
    test1["two"] = "golang"
    test1["three"] = "java"
    fmt.Println(test1) //map[two:golang three:java one:php]








    //第二种声明
    test2 := make(map[string]string)
    test2["one"] = "php"
    test2["two"] = "golang"
    test2["three"] = "java"
    fmt.Println(test2) //map[one:php two:golang three:java]






    //第三种声明
    test3 := map[string]string{
        "one" : "php",
        "two" : "golang",
        "three" : "java",
    }
    fmt.Println(test3) //map[one:php two:golang three:java]






    //value中也可以嵌套map结构
    language := make(map[string]map[string]string)
    language["php"] = make(map[string]string, 2)
    language["php"]["id"] = "1"
    language["php"]["desc"] = "php是世界上最美的语言"
    language["golang"] = make(map[string]string, 2)
    language["golang"]["id"] = "2"
    language["golang"]["desc"] = "golang抗并发非常good"
    
    fmt.Println(language) //map[php:map[id:1 desc:php是世界上最美的语言] golang:map[id:2 desc:golang抗并发非常good]]


    //增删改查
    // val, key := language["php"]  //查找是否有php这个子元素
    // if key {
    //     fmt.Printf("%v", val)
    // } else {
    //     fmt.Printf("no");
    // }

    //language["php"]["id"] = "3" //修改了php子元素的id值
    //language["php"]["nickname"] = "啪啪啪" //增加php元素里的nickname值
    //delete(language, "php")  //删除了php子元素
    fmt.Println(language)
}

6、面向对象特征

方法

go语言中存在指针的概念,所以方法的传参存在传入值和传入指针的区别。

package main




















































import "fmt"
































//定义一个结构体
type T struct {
    name string
}







func (t T) method1() {
    t.name = "new name1"
}













func (t *T) method2() {
    t.name = "new name2"
}





func main() {
    t := T{"old name"}






    fmt.Println("method1 调用前 ", t.name)
    t.method1()
    fmt.Println("method1 调用后 ", t.name)



    fmt.Println("method2 调用前 ", t.name)
    t.method2()
    fmt.Println("method2 调用后 ", t.name)
}


输出结果为:

method1 调用前  old name
method1 调用后  old name
method2 调用前  old name
method2 调用后  new name2

方法值和方法表达式

方法值

我们经常选择一个方法,并且在同一个表达式里执行,比如常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法”值”一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器,只要传入函数的参数即可:

package main




















































import "fmt"








import "math"













type Point struct{ X, Y float64 }



//这是给struct Point类型定义一个方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}











func main() {








    p := Point{1, 2}
    q := Point{4, 6}




    distanceFormP := p.Distance   // 方法值(相当于C语言的函数地址,函数指针)
    fmt.Println(distanceFormP(q)) // "5"
    fmt.Println(p.Distance(q))    // "5"






    //实际上distanceFormP 就绑定了 p接收器的方法Distance


    distanceFormQ := q.Distance   //
    fmt.Println(distanceFormQ(p)) // "5"
    fmt.Println(q.Distance(p))    // "5"



    //实际上distanceFormQ 就绑定了 q接收器的方法Distance
}


在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话,方法”值”会非常实用。

方法表达式

和方法”值”相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器(p.Distance)语法来指定方法的接收器。

当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数”值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:

package main




















































import "fmt"








import "math"























type Point struct{ X, Y float64 }













//这是给struct Point类型定义一个方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}













func main() {





    p := Point{1, 2}
    q := Point{4, 6}








    distance1 := Point.Distance //方法表达式, 是一个函数值(相当于C语言的函数指针)
    fmt.Println(distance1(p, q))
    fmt.Printf("%T\n", distance1) //%T表示打出数据类型




    distance2 := (*Point).Distance //方法表达式,必须传递指针类型
    distance2(&p, q)
    fmt.Printf("%T\n", distance2)


}


输出结果为:

5
func(main.Point, main.Point) float64
func(*main.Point, main.Point) float64

7、interface与类型断言

Golang的语言中提供了断言的功能。golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。

Java和Go都有接口(interface)的概念,但它们的表现形式和功能有些不同。

Java中的接口

在Java中,接口是一种引用类型,它定义了一组方法的集合。实现(implements)这些接口的类必须提供这些方法的具体实现。一个类可以实现多个接口,也就是说Java通过接口提供了一种有限的形式的多重继承。

下面是一个Java接口的例子:

interface Animal {
   void eat();
   void sleep();
}













class Dog implements Animal {
   public void eat() {
       System.out.println("Dog eats");
   }








   public void sleep() {
       System.out.println("Dog sleeps");
   }
}













在这个例子中,Dog类实现了Animal接口,所以它必须提供eat()和sleep()两个方法的具体实现。

Go中的接口

在Go中,接口是一种类型,它定义了一组方法(正如在Java中一样),但的定义方式和使用方式略有不同。Go的接口是隐式实现的,也就是说我们无需明确指定某个类型实现了该接口(即无需像Java中那样使用implements关键字),只要一个类型提供了接口所有的方法即表示它实现了该接口。

这是一个Go接口的例子:

type Animal interface {
   Eat()
   Sleep()
}













type Dog struct {}










func (d Dog) Eat() {
   fmt.Println("Dog eats")
}







func (d Dog) Sleep() {
   fmt.Println("Dog sleeps")
}













在这个例子中,Dog类型提供了Eat()和Sleep()方法,因此,它隐式地满足了Animal接口。

func funcName(a interface{}) string {
     return string(a)
}

编译器会返回

cannot convert a (type interface{}) to type string: need type assertion

此时,意味着整个转化的过程需要类型断言。类型断言有以下几种形式:

1)直接断言使用

var a interface{}





















fmt.Println("Where are you,Jonny?", a.(string))

但是如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断

value, ok := a.(string)
if !ok {
    fmt.Println("It's not ok for type string")
    return
}
fmt.Println("The value is ", value)

2)配合switch使用

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T", t)       // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}





8、反射

编程语言中反射的概念

在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用。

Golang的gRPC也是通过反射实现的。

interface 和 反射

在讲反射之前,先来看看Golang关于类型设计的一些原则

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.

接下来要讲的反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型(type,value):

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型对应concrete type,另外一个指针指向实际的值对应value

Golang的反射reflect

reflect的基本功能TypeOf和ValueOf

既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释:

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
func ValueOf(i interface{}) Value {...}



//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0






















// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}






//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:

package main





































import (




    "fmt"




    "reflect"




)














func main() {


    var num float64 = 1.2345









    fmt.Println("type: ", reflect.TypeOf(num))
    fmt.Println("value: ", reflect.ValueOf(num))
}




运行结果为:

type:  float64
value:  1.2345

从relfect.Value中获取接口interface的信息

当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。

已知类型

已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:

realValue := value.Interface().(已知的类型)

示例如下:

package main





































import (




    "fmt"




    "reflect"




)














func main() {


    var num float64 = 1.2345









    pointer := reflect.ValueOf(&num)
    value := reflect.ValueOf(num)







    // 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
    // Golang 对类型要求非常严格,类型一定要完全符合
    // 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
    convertPointer := pointer.Interface().(*float64)
    convertValue := value.Interface().(float64)





    fmt.Println(convertPointer)
    fmt.Println(convertValue)
}






运行结果为:

0xc42000e238
1.2345

未知类型

很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:

package main





































import (




    "fmt"




    "reflect"




)














type User struct {

    Id   int

    Name string

    Age  int

}











func (u User) ReflectCallFunc() {
    fmt.Println("Allen.Wu ReflectCallFunc")
}






func main() {





    user := User{1, "Allen.Wu", 25}




    DoFiledAndMethod(user)





}




// 通过接口来获取任意参数,然后一一揭晓
func DoFiledAndMethod(input interface{}) {



    getType := reflect.TypeOf(input)
    fmt.Println("get Type is :", getType.Name())

    getValue := reflect.ValueOf(input)
    fmt.Println("get all Fields is:", getValue)


    // 获取方法字段
    // 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
    // 2. 再通过reflect.Type的Field获取其Field
    // 3. 最后通过Field的Interface()得到对应的value
    for i := 0; i < getType.NumField(); i++ {
        field := getType.Field(i)
        value := getValue.Field(i).Interface()
        fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }

    // 获取方法
    // 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
    for i := 0; i < getType.NumMethod(); i++ {
        m := getType.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}

运行结果为:

get Type is : User
get all Fields is: {1 Allen.Wu 25}
Id: int = 1
Name: string = Allen.Wu
Age: int = 25
ReflectCallFunc: func(main.User)

通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

package main





































import (




    "fmt"




    "reflect"




)














func main() {







    var num float64 = 1.2345
    fmt.Println("old value of pointer:", num)





    // 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
    pointer := reflect.ValueOf(&num)
    newValue := pointer.Elem()



    fmt.Println("type of pointer:", newValue.Type())
    fmt.Println("settability of pointer:", newValue.CanSet())





    // 重新赋值
    newValue.SetFloat(77)
    fmt.Println("new value of pointer:", num)





    ////////////////////
    // 如果reflect.ValueOf的参数不是指针,会如何?
    pointer = reflect.ValueOf(num)
    //newValue = pointer.Elem() 
    // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}







运行结果为:

old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77

说明

  1. 需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针
  2. 如果传入的参数不是指针,而是变量,那么
    • 通过Elem获取原始值对应的对象则直接panic
    • 通过CanSet方法查询是否可以设置返回false
  1. newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经 修改了。
  2. reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的

通过reflect.ValueOf来进行方法的调用

在工程应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法函数的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定。

类似于Java中的动态代理

package main





































import (




    "fmt"




    "reflect"




)














type User struct {

    Id   int

    Name string

    Age  int

}











func (u User) ReflectCallFuncHasArgs(name string, age int) {
    fmt.Println("ReflectCallFuncHasArgs name: ", name, ", age:", age, "and origal User.Name:", u.Name)
}






func (u User) ReflectCallFuncNoArgs() {
    fmt.Println("ReflectCallFuncNoArgs")
}




// 如何通过反射来进行方法的调用?
// 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call


func main() {
    user := User{1, "Allen.Wang", 25}
    
    // 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
    getValue := reflect.ValueOf(user)






    // 一定要指定参数为正确的方法名
    // 2. 先看看带有参数的调用方法
    methodValue := getValue.MethodByName("ReflectCallFuncHasArgs")
    args := []reflect.Value{reflect.ValueOf("wanghaonan"), reflect.ValueOf(20)}
    methodValue.Call(args)


    // 一定要指定参数为正确的方法名
    // 3. 再看看无参数的调用方法
    methodValue = getValue.MethodByName("ReflectCallFuncNoArgs")
    args = make([]reflect.Value, 0)
    methodValue.Call(args)
}

运行结果为:

ReflectCallFuncHasArgs name:  wanghaonan , age: 20 and origal User.Name: Allen.Wang
ReflectCallFuncNoArgs

Golang的反射reflect性能

Golang的反射很慢,这个和它的API设计有关。在 java 里面,我们一般使用反射都是这样来弄的。

Field field = clazz.getField("hello");
field.get(obj1);
field.get(obj2);

这个取得的反射对象类型是 java.lang.reflect.Field。它是可以复用的。只要传入不同的obj,就可以取得这个obj上对应的 field。

但是Golang的反射不是这样设计的:

type_ := reflect.TypeOf(obj)
field, _ := type_.FieldByName("hello")

这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。如果要取值,得用另外一套对object,而不是type的反射

type_ := reflect.ValueOf(obj)
fieldValue := type_.FieldByName("hello")

这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYBNxjBr' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片