Go基础之指针和反射讲解
1 指针
学习该篇之前可以先看看 C语言的指针,点击此处了解 C语言指针 讲解
1.1 简介
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
一个指针变量指向了一个值的内存地址。
类似于变量和常量,在使用指针前需要声明指针。指针声明格式如下:
var var_name *var-type
var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。
当一个指针被定义后没有分配到任何变量时,它的值为 nil,也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
1.2 使用指针
指针使用流程:
- 定义指针变量。
- 为指针变量赋值。
- 访问指针变量中指向地址的值。
指针解引用:在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
package main
import "fmt"
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
1.3 指针优化输出
1.3.1 优化输出复杂类型
在 Go 中,指针优化主要体现在 Go 语言对某些类型(如数组、切片、结构体)输出时的格式化处理。这种优化并不适用于单个基本数据类型(如 int、string)的地址输出。
number := [3]int{5, 6, 7}
fmt.Println("number数组的值的地址:", &number)
输出结果
number数组的值的地址: &[5 6 7]
Go 语言对数组类型指针(*[N]T)的格式化输出进行了优化,直接显示数组的内容(即 &[5, 6, 7])。
当指针指向一个数组时,fmt 包会解引用该指针并显示数组内容,而不是打印数组指针的原始地址。
主要是,Go 的设计者希望输出数组指针时,开发者能快速看到数组的内容,而不是内存地址。
1.3.2 去掉优化
如果不希望 &[5, 6, 7] 优化输出,可以打印指针本身:
fmt.Printf("数组的真实地址: %p\n", &number)
// 输出:数组的真实地址: 0xc000014080
1.3.3 基本类型
没有优化基本类型:
var a int = 1
fmt.Println("&a:", &a)
输出结果
&a: 0xc000014088
对于基本类型(如 int、float 等),&a 只表示 a 的地址,而没有特殊优化来显示值。
基本类型输出指针变量(如 &a)时,fmt.Println 默认不会尝试解引用并显示值,而是直接显示地址。这是 Go 语言的设计原则,确保基本数据类型的行为一致。
1.4 指针数组
如果需要保存数组,这样我们就需要使用到指针。
声明 整型指针数组:var ptr [MAX]*int;
ptr 为整型指针数组。因此每个元素都指向了一个值。以下实例的三个整数将存储在指针数组中:
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
以上代码执行输出结果为:
a[0] = 10
a[1] = 100
a[2] = 200
使用range遍历指针数组
const max = 3
func main(){
number := [max]int{5, 6, 7}
var ptrs [max]*int //指针数组
//将number数组的值的地址赋给ptrs
fmt.Println("number数组的值的地址:", &number)
for i, x := range &number {
fmt.Println(i, x)
ptrs[i] = &x
}
for i, x := range ptrs {
fmt.Printf("指针数组:索引:%d 值:%d 值的内存地址:%d\n", i, *x, x)
}
}
结果:
number数组的值的地址: &[5 6 7]
0 5
1 6
2 7
指针数组:索引:0 值:5 值的内存地址:824633762096
指针数组:索引:1 值:6 值的内存地址:824633762104
指针数组:索引:2 值:7 值的内存地址:824633762112
1.4.1 指针数组优化
(*arr)[0] 可以替换为 arr[0]
package main
import (
"fmt"
)
func modify(arr *[3]int) {
(*arr)[0] = 90
//或者用这样的 arr[0] = 90
}
func main() {
a := [3]int{ 89, 90, 91}
modify(&a)
fmt.Println(a)
}
1.5 指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址
指向指针的指针变量声明格式如下:var ptr **int;
以上指向指针的指针变量为整型。
访问指向指针的指针变量值需要使用 两个 *号,如下所示:
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
还可以多级递增,比如:指向指针的指针的指针
func main(){
var a int = 1
var ptr1 *int = &a
var ptr2 **int = &ptr1
var ptr3 **(*int) = &ptr2 // 也可以写作:var ptr3 ***int = &ptr2
// 依次类推
fmt.Println("a:", a)
fmt.Println("ptr1", ptr1)
fmt.Println("ptr2", ptr2)
fmt.Println("ptr3", ptr3)
fmt.Println("*ptr1", *ptr1)
fmt.Println("**ptr2", **ptr2)
fmt.Println("**(*ptr3)", **(*ptr3)) // 也可以写作:***ptr3
}
1.6 向函数传递指针参数
Go 允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。
以下实例演示了如何向函数传递指针,并在函数调用后修改函数内的值,:
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 )
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &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 */
}
2 反射
Golang语言实现的反射机制就是指在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的。
go的变量包括type, value两部分,type 包括static type和concrete type(static type是在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型)。
类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.
反射是建立在类型之上的,Golang指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type) 只有interface类型才有反射一说
反射就是用来检测存储在接口变量内部(值value;类型concrete type) 的一种机制。
2.1 reflect
主要方法:
reflect.TypeOf:获取变量类型,返回reflect.Type类型;如果接口为空则返回nilreflect.ValueOf:获取变量的值,返回reflect.Value类型;如果接口为空则返回0reflect.Value.Kind:获取变量的类别,返回一个常量;表示该类型的特定种类reflect.Value.Interface():转换成interface{}类型;NumField()方法返回结构体中字段的数量,而Field(i int)方法返回字段 i 的reflect.Value,其中 i 是字段的索引。在 Go 的反射(reflect)机制中,结构体的字段是按定义顺序分配索引的,索引从 0 开始
2.1.1 示例
获取变量类型
package main
import (
"fmt"
"reflect"
)
func test(i interface{}) {
// 反射获取值类型
t := reflect.TypeOf(i)
fmt.Println(t)
// 反射获取值
v := reflect.ValueOf(i)
fmt.Println(v)
}
func main() {
var a int = 10
test(a)
}
/*
int
10
*/
类型和类别
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
Score float32
}
func test(i interface{}) {
// 反射获取类型
t := reflect.TypeOf(i)
fmt.Println("类型:",t)
// 反射获取类别
v := reflect.ValueOf(i)
k := v.Kind()
fmt.Println("类别:",k)
}
func main() {
var stu Student = Student {
Name: "zhangsan",
Age: 19,
Score: 88,
}
test(stu)
}
/*
类型: main.Student
类别: struct
*/
示例:断言处理类型转化
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
Score float32
}
func test(i interface{}) {
// 反射获取类型
t := reflect.TypeOf(i)
fmt.Println("类型:",t)
// 反射获取类别
v := reflect.ValueOf(i)
k := v.Kind()
fmt.Println("类别:",k)
// 转换成接口类型
iv := v.Interface()
// 断言
stu,ok := iv.(Student)
if ok {
fmt.Printf("结构:%v 类型:%T\n",stu,stu)
}
}
func main() {
var stu Student = Student {
Name: "zhangsan",
Age: 19,
Score: 88,
}
test(stu)
}
/*
类型: main.Student
类别: struct
结构:{zhangsan 19 88} 类型:main.Student
*/
2.2 获取变量值 ValueOf
获取变量值:
- reflect.ValueOf(x).Float()
- reflect.ValueOf(x).Int()
- reflect.ValueOf(x).String()
- reflect.ValueOf(x).Bool()
示例:类型转换
package main
import (
"fmt"
"reflect"
)
func testInt(b interface{}) {
val := reflect.ValueOf(b)
fmt.Printf("val 类型:%T\n",val)
// 转换成 int 类型,其他类型都可以转化成 string
a := val.Int()
fmt.Printf("a 类型:%T\n",a)
}
func main() {
testInt(100)
}
/*
val 类型:reflect.Value
a 类型:int64
*/
2.3 修改变量值 Value.Set
设置变量值:
- reflect.Value.SetFloat() #设置浮点数
- reflect.Value.SetInt() #设置整数
- reflect.Value.SetString() #设置字符串
2.3.1 Elem 方法
Elem() 方法可以用来获取反射值指向的对象的 元素,通常在要通过反射修改一个值时,需要获取指向该值的指针,并且需要通过 Elem() 来获取其底层值。
那么什么时候使用 Elem()?
Elem()方法用于从指针类型的反射值获取它指向的对象的值。也就是说,如果通过反射获取的是一个指向某个对象的指针类型(比如*int),然后想修改这个对象的值,就需要使用Elem()来获取该指针指向的实际值。
package main
import (
"fmt"
"reflect"
)
func main() {
a := 10
fmt.Println("Before:", a)
// 获取 a 的反射值
v := reflect.ValueOf(&a)
// 通过 Elem() 获取指针指向的值并修改
v.Elem().SetInt(20)
fmt.Println("After:", a) // 输出 20
}
示例:修改值
package main
import (
"fmt"
"reflect"
)
func testInt(b interface{}) {
val := reflect.ValueOf(b)
// 更改值需要value的地址,否则会崩溃,Elem() 表示 *
val.Elem().SetInt(100)
c := val.Elem().Int()
fmt.Printf("类型:%T,值:%d\n",c,c)
}
func main() {
var i = 10
testInt(&i)
fmt.Println(i)
}
/*
类型:int64,值:100
100
*/
2.3.2 修改结构体反射
对于结构体中的字段,如果你想修改它们,也需要使用 Elem() 来获取该结构体指针的底层值。
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
fmt.Println("Before:", p)
v := reflect.ValueOf(&p) // 获取结构体指针的反射值
// 修改结构体字段
v.Elem().FieldByName("Name").SetString("Bob")
v.Elem().FieldByName("Age").SetInt(40)
fmt.Println("After:", p) // 输出 {Bob 40}
}
解释:
reflect.ValueOf(&p):获取的是 Person 结构体指针的反射值。使用 v.Elem():获取该结构体指针指向的实际结构体值。FieldByName("Name")和FieldByName("Age"):获取到结构体字段的反射值,然后使用SetString()和SetInt()来修改字段的值。
示例:反射出结构体属性和方法数量
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
Score float32
}
func (s Student) Sleep() {
fmt.Println("正在睡觉")
}
func (s Student) Run(min interface{ }) {
fmt.Printf("跑步%d分钟",min)
}
// 断言结构体
func Test(object interface{ }) {
// 获取值
v := reflect.ValueOf(object)
// 获取类别
c := v.Kind()
// 判断类别是否为结构体
if c != reflect.Struct {
fmt.Println("expect struct")
return
}
// 获取结构体字段数量
num := v.NumField()
fmt.Println("字段数量:",num)
// 获取结构体方法数量
numOfMethod := v.NumMethod()
fmt.Println("方法数量:",numOfMethod)
}
func main() {
var stu Student = Student {
Name: "zhangsan",
Age: 18,
Score: 90,
}
Test(stu)
}
/*
字段数量: 3
方法数量: 2
*/
2.4 函数反射
2.4.1 简介
示例:Go 中函数可以赋值给变量。
package main
import "fmt"
func Hello() {
fmt.Println("hello world")
}
func main() {
// 函数可以赋值给变量
a := Hello
a()
}
/*
hello world
*/
2.4.2 无参调用
函数可以像普通的类型变量一样,在反射机制中就和不同的变量是一样,在反射中函数和方法的类型 (Type) 都是 reflect.Func,如果要调用函数,通过 Value 的 Call() 方法
package main
import (
"fmt"
"reflect"
)
func Hello() {
fmt.Println("hello world")
}
func main() {
// 反射获取 hello
v := reflect.ValueOf(Hello)
fmt.Printf("类型:%T\n",v)
// 判断 hello 的类型
if v.Kind() == reflect.Func {
fmt.Println("OK")
}
// 反射调用函数
v.Call(nil)
}
/*
类型:reflect.Value
OK
hello world
*/
2.4.3 有参有返回调用
Value 的 Call() 方法的参数是一个 Value 的 slice,对应的反射函数类型的参数,返回值也是一个 Value 的 slice,同样对应反射函数类型的返回值。
package main
import (
"fmt"
"reflect"
"strconv"
)
func prints(i int) string {
fmt.Println("i=",i)
return strconv.Itoa(i)
}
func main() {
fv := reflect.ValueOf(prints)
// 定义参数切片
params := make([]reflect.Value,1)
// 参数为 20
params[0] = reflect.ValueOf(20)
// 反射函数,获取处理结果
result := fv.Call(params)
fmt.Printf("result 类型:%T\n",result)
fmt.Printf("result 转换后类型:%T,值是:%s\n",result[0].Interface().(string),result[0].Interface().(string))
}
/*
i= 20
result 类型:[]reflect.Value
result 转换后类型:string,值是:20
*/
2.5 方法反射
函数和方法可以说其实本质上是相同的,只不过方法与一个对象进行了绑定,方法是对象的一种行为,这种行为是对于这个对象的一系列操作,例如修改对象的某个属性。
2.5.1 MethodByName
MethodByName 是用来通过方法的 名称 来查找并获取该方法的反射对象。是通过字符串名称来获取方法的。
语法:func (v Value) MethodByName(name string) (Method, bool)
v: 是一个反射值,表示一个结构体类型的对象。name: 方法的名称,是一个字符串。
返回值:
Method: 表示方法的反射对象。bool: 表示方法是否存在。如果方法存在,返回 true,否则返回 false。
示例
package main
import (
"fmt"
"reflect"
)
type MyStruct struct{}
func (m MyStruct) Greet(name string) {
fmt.Println("Hello", name)
}
func main() {
m := MyStruct{}
// 获取反射值
v := reflect.ValueOf(m)
// 获取方法名为 "Greet" 的反射对象
method, exists := v.MethodByName("Greet")
if exists {
// 调用方法
method.Call([]reflect.Value{reflect.ValueOf("Alice")}) // 输出: Hello Alice
} else {
fmt.Println("Method not found")
}
}
package main
import (
"fmt"
"reflect"
"strconv"
)
type Student struct {
age int
name string
}
func (s *Student) SetAge(i int) {
s.age = i
}
func (s *Student) SetName(name string) {
s.name = name
}
func (s *Student) String() string {
return fmt.Sprintf("%p",s) + "--name:" + s.name + " age:" + strconv.Itoa(s.age)
}
func main() {
// 实例化
stu := &Student{22,"zhangsan"}
// 反射获取值,指针方式
stuV := reflect.ValueOf(&stu).Elem()
// 也可以使用非指针(值类型) stuV := reflect.ValueOf(stu)
fmt.Println("Before:",stuV.MethodByName("String").Call(nil)[0])
// 修改值
params := make([]reflect.Value,1)
params[0] = reflect.ValueOf(18)
stuV.MethodByName("SetAge").Call(params)
params[0] = reflect.ValueOf("lisi")
stuV.MethodByName("SetName").Call(params)
fmt.Println("After:",stuV.MethodByName("String").Call(nil)[0])
}
2.5.2 Method
Method 是用来通过反射值获取某个方法的反射对象,它需要一个方法的 索引 来定位目标方法。返回的是一个 reflect.Method,其中包含了方法的信息(如方法类型、方法值等)。
语法:func (v Value) Method(i int) Method
v: 是一个反射值,表示一个结构体类型的对象。i: 是方法的索引,方法的索引是从 0 开始的。它是由方法在类型中的定义顺序来决定的,方法的索引值是按声明顺序分配的。
例如,如果你有一个结构体 MyStruct,它有三个方法,那么 i = 0 会返回第一个方法。
package main
import (
"fmt"
"reflect"
)
type MyStruct struct{}
func (m MyStruct) Greet(name string) {
fmt.Println("Hello", name)
}
func main() {
m := MyStruct{}
// 获取反射值
v := reflect.ValueOf(m)
// 获取第一个方法的反射对象
method := v.Method(0)
// 调用方法
method.Call([]reflect.Value{reflect.ValueOf("Alice")}) // 输出: Hello Alice
}
2.6 reflect性能
Golang的反射很慢,Golang reflect慢主要有两个原因:
- 涉及到内存分配以及后续的GC;
- reflect实现里面有大量的枚举,也就是for循环,比如类型之类的;
这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。
type_ := reflect.TypeOf(obj)
field, _ := type_.FieldByName("hello")
如果要取值,得用另外一套对object,而不是type的反射,fieldValue类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。
type_ := reflect.ValueOf(obj)
fieldValue := type_.FieldByName("hello")

浙公网安备 33010602011771号