首先明确一点,Go和Java中的参数传递方式都是值传递,且两者中的参数都分为值类型和引用类型
值类型和引用类型
在编程语言中,值类型(value types)和引用类型(reference types)是两种不同的数据类型分类方式,用于描述数据在内存中的存储和传递方式。
值类型(Value Types):
- 值类型的变量直接包含其数据的实际值,而不是对数据的引用或地址。
- 当你创建一个值类型的变量时,该变量存储的是数据的实际值。
- 当将值类型的变量传递给函数时,传递的是值的副本,而不是原始值本身。
在值类型中,操作的是值本身,对变量的操作不会影响其他变量。
引用类型(Reference Types):
- 引用类型的变量存储的是对数据的引用或地址,而不是数据的实际值。
- 当你创建一个引用类型的变量时,该变量存储的是数据的地址,即指向数据的引用。
- 当将引用类型的变量传递给函数时,传递的是引用的副本,即一个新的引用变量,但它们指向的是同一个数据。
在引用类型中,操作的是指向数据的引用,因此对数据的修改会影响所有引用该数据的变量。
综上所述,值类型和引用类型的主要区别在于数据在内存中的存储方式以及在传递和操作时的行为。值类型直接存储数据的实际值,而引用类型存储对数据的引用,即指向数据的地址。
在Go中,除了基础数据类型和派生类型,其余的类型都是引用类型,指针属于值类型(下方代码讨论)
值类型:
- 基本数据类型:
- int、int8、int16、int32、int64
- uint、uint8、uint16、uint32、uint64
- float32、float64
- bool
- byte(uint8的别名)
- rune(int32的别名,表示Unicode代码点)
- complex64、complex128(复数类型)
- 派生类型:
- 数组(Array)
- 结构体(Struct)
引用类型:
- 切片(Slice)
- 映射(Map)
- 通道(Channel)
- 接口(Interface)
- 函数类型(Function)
在Java中,值类型就是基础数据类型,
基本数据类型:
- 整数类型:
- byte
- short
- int
- long
- 浮点数类型:
- float
- double
- 布尔类型:
- boolean
- 字符类型:
- char
引用类型:
- 类(Class):即通过**
class
**关键字定义的自定义类型。 - 接口(Interface):即通过**
interface
**关键字定义的接口类型。 - 数组(Array):即通过**
[]
**语法定义的数组类型。 - 枚举(Enum):即通过**
enum
**关键字定义的枚举类型。 - 泛型(Generics):即通过泛型类和泛型方法定义的类型。
参数传递
package main
import "fmt"
type student struct{
age int
}
func main() {
//结论1:Go语言中struct为值类型,作为函数参数时,传递的是值的拷贝
var stu1 student
stu1.age = 1
fmt.Printf("stu1在主函数中的地址1:%p\n",&stu1)
changeAge(stu1)
fmt.Printf("stu1在主函数中的地址2:%p\n",&stu1)
fmt.Printf("stu1.age:%d\n",stu1.age)
fmt.Println("------")
//结论3:指针为值类型
var stu2 *student
fmt.Printf("stu2在主函数中的地址1:%p\n",&stu2)
fmt.Printf("stu2指针存储的地址1:%p\n",stu2)
stu2 = new (student)
fmt.Printf("stu2指针存储的地址2:%p\n",stu2)
fmt.Printf("stu2在主函数中的地址2:%p\n",&stu2)
stu2.age = 2
changeAge2(stu2)
fmt.Printf("stu2在主函数中的地址3:%p\n",&stu2)
fmt.Printf("stu2.age:%d\n",stu2.age)
//结论4: map为引用类型
fmt.Println("------")
var map1 map[int]string
map1 = make(map[int]string)
map1[1] = "one"
fmt.Printf("map1在主函数中的地址1:%p\n",&map1)
changeMap(map1)
fmt.Println(map1[1])
}
// 值类型参数
func changeAge(stu student){
fmt.Printf("stu1在changeAge函数中的地址:%p\n",&stu)
stu.age = 1;
stu = student{11}
fmt.Printf("stu1在changeAge函数中指向新的结构体后的地址:%p\n",&stu)
// 结论2:值类型在修改值时,不会改变地址
fmt.Printf("stu.age:%d\n",stu.age)
}
// 指针类型参数
func changeAge2(stu *student){
fmt.Printf("stu2在changeAge2函数中的地址:%p\n",&stu)
fmt.Printf("stu2指针changeAge2存储的地址1:%p\n",stu)
stu.age = 22
stu = new(student)
fmt.Printf("stu2指针changeAge2存储的地址2:%p\n",stu)
fmt.Printf("stu2在changeAge2函数中指向新的结构体后的地址:%p\n",&stu)
stu.age = 222
fmt.Printf("stu.age:%d\n",stu.age)
}
func changeMap(map1 map[int]string){
fmt.Printf("map1在changeMap中的地址1:%p\n",&map1)
map1[1] = "one2"
}
输出
stu1在主函数中的地址1:0xc00000a0b8
stu1在changeAge函数中的地址:0xc00000a0e0
stu1在changeAge函数中指向新的结构体后的地址:0xc00000a0e0
stu.age:11
stu1在主函数中的地址2:0xc00000a0b8
stu1.age:1
------
stu2在主函数中的地址1:0xc000048030
stu2指针存储的地址1:0x0
stu2指针存储的地址2:0xc00000a0e8
stu2在主函数中的地址2:0xc000048030
stu2在changeAge2函数中的地址:0xc000048038
stu2指针changeAge2存储的地址1:0xc00000a0e8
stu2指针changeAge2存储的地址2:0xc00000a0f0
stu2在changeAge2函数中指向新的结构体后的地址:0xc000048038
stu.age:222
stu2在主函数中的地址3:0xc000048030
stu2.age:22
------
map1在主函数中的地址1:0xc000048040
map1在changeMap中的地址1:0xc000048048
one2
结论1:Go语言中struct为值类型,作为函数参数时,传递的是值的拷贝,在changeAge函数中接受到的stu地址已经发生了改变,且任何操作都无法对主函数中的stu1造成影响
结论2:值类型在修改值时,不会改变地址
结论3:指针为值类型,指针在传递时同样进行了拷贝,但指针存储的是stu2在内存中的地址,故主函数中的stu2和changeAge2中指向的为同一片内存空间,在changeAge2中进行操作stu.age = 22,同样会影响主函数中的stu2.age,同时在changeAge2中进行操作stu = new(student),改变的时stu中存储的地址,并不影响stu自身的地址
结论4:map为引用类型,在changeMap中的修改对主函数中的map同样生效
指针是值类型是为了保持Go语言的简洁性和一致性,并且与其他值类型的行为一致。
在Go语言中,指针是一种特殊的数据类型,它存储了一个变量的内存地址。当你创建一个指针变量时,实际上创建的是一个值,这个值是某个内存地址。因此,指针变量存储的是内存地址的值,而不是内存地址本身。
将指针视为值类型有几个优点:
- 简单性:指针是值类型的一种,这使得Go语言的类型系统更加简洁和统一。你可以像处理其他值类型一样来处理指针类型,而不需要特殊的规则或语法。
- 一致性:在Go语言中,所有的变量都是值。通过将指针视为值类型,可以保持语言的一致性。无论是基本数据类型、结构体、数组还是指针,它们都是值,都可以通过传值来传递。
- 可预测性:值类型的行为更加可预测。当你将指针传递给函数时,传递的是指针的副本,而不是指针所指向的内存地址。这意味着函数无法直接修改原始指针的值,从而减少了意外的修改和错误。
尽管指针是值类型,但通过指针可以实现引用语义,即通过指针可以修改原始数据。因为指针存储的是数据的地址,通过传递指针可以让多个变量共享同一个内存地址,从而实现对数据的共享访问和修改。
对比
在Java中同理,基础类型传递的是该基础类型的拷贝,无法影响主函数的基础类型,而拥有指针的go语言则可以通过传递基础类型的指针,实现对主函数中基础类型的修改。
/**
在这个示例中,change方法接收一个基本数据类型int作为参数,并在方法内部将其值修改为10。
但是在main方法中,num的值不会被改变,因为在Java中基本数据类型作为参数传递给方法时,
传递的是值的副本,而不是原始值本身。因此,在change方法中对x的修改不会影响num的值。
*/
public class Main {
public static void main(String[] args) {
int num = 42;
System.out.println("Before change method: " + num);
change(num);
System.out.println("After change method: " + num);
}
public static void change(int x) {
x = 10;
}
}
Java中引用类型的参数传递同样与go中的引用类型相同,但是要注意java中对象与go中结构体的不用,前者为引用类型,后者为值类型。
具体表现可以参考上面的go参数传递代码,struct结构体作为参数时,传递的是拷贝,在函数内的任何修改都不会影响原结构体。
也就是说,如果想在方法内修改结构体,则需要传递该结构体的指针,指针在传递时进行拷贝,但指针中存储的内存地址没有发生改变,故修改的是同一快内存空间。
同时我们注意到,java中对象在方法中的修改会影响到方法外的对象,没错,java中对象的传递和指针是一样的。。。
/**
*在这个示例中,自定义类 MyInteger 中的 value 成员变量被修改为 10。
在 main 方法中,num 的值也随之改变,因为 MyInteger 是引用类型,
作为参数传递给方法时,传递的是引用的副本,即指向同一个对象的引用。
因此,在 change 方法中对 x 的 setValue() 操作会修改 main 方法中的 num 的值。
*/
class MyInteger {
int value;
public MyInteger(int value) {
this.value = value;
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
MyInteger num = new MyInteger(42);
System.out.println("Before change method: " + num.getValue());
change(num);
System.out.println("After change method: " + num.getValue());
}
public static void change(MyInteger x) {
x.setValue(10);
}
}
优缺点对比
在Java中,当我们在代码中看到方法调用change(stu),我们无法判断,stu变量是否在change方法中进行了修改,而在go中 change(stu),change中对stu的修改无法影响到方法调用位置的stu数据,只有传递指针change(&stu),stu才有可能在方法中被修改。此处可以提高代码的简单性和可读性,这是优点,同时也存在缺点, change(stu)对stu结构体进行了拷贝,相较于拷贝内存地址,该操作可能需要花费更多的时间和内存空间。