💡
Go 的核心原则:一切皆值传递。Go 没有引用传递,但你可以显式传递指针(*T)来达到修改原始值的效果。
值传递的情况(所有基本类型 + 数组 + 结构体)
- 基本类型:
int, float64, bool, string
- 数组:
[5]int(注意:数组不是切片!)
- 结构体:
struct(整个结构体被复制)
显式传指针(等效于引用传递效果)
- 传递
*int, *string, *MyStruct 等指针类型
- 在函数内通过
*ptr 解引用后修改,影响原始变量
⚠ 特殊类型:slice、map、channel(传的是描述符)
- slice:传的是描述符(ptr + len + cap),修改元素会影响原始 slice;但 append 超出容量后不影响
- map:传的是哈希表的指针,函数内 add/delete 键会影响原始 map
- channel:传的是内部指针,共享同一个通道
package main
import "fmt"
// ① 值传递 —— 修改不影响原始值
func modifyInt(n int) {
n = 100
}
// ② 指针传递 —— 修改影响原始值
func modifyIntPtr(n *int) {
*n = 100
}
// ③ 结构体值传递 —— 整体复制
type Point struct { X, Y int }
func modifyPoint(p Point) { p.X = 999 }
// ④ 结构体指针 —— 修改影响原始
func modifyPointPtr(p *Point) { p.X = 999 }
// ⑤ slice —— 修改元素影响原始,append超容量不影响
func modifySlice(s []int) {
s[0] = 99 // ✅ 影响原始 slice
s = append(s, 1000) // ❌ 超容量后不影响原始
}
// ⑥ map —— 共享底层哈希表
func modifyMap(m map[string]int) {
m["key"] = 999 // ✅ 影响原始 map
}
func main() {
a := 42
modifyInt(a)
fmt.Println(a) // 42 ← 没变
modifyIntPtr(&a)
fmt.Println(a) // 100 ← 改了
p := Point{1, 2}
modifyPoint(p)
fmt.Println(p.X) // 1 ← 没变(结构体是副本)
modifyPointPtr(&p)
fmt.Println(p.X) // 999 ← 改了
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // [99 2 3] ← 元素被改了,但 append 部分没有
}
✅
Go 设计哲学:显式优于隐式。需要修改原始值,就明确传指针 &x,不存在隐式引用传递的歧义。
💡
PHP 默认值传递,使用 & 符号显式开启引用传递。对象比较特殊:传的是「对象标识符的副本」,行为类似引用但本质是值传递。
值传递的情况
- 标量类型:
int, float, string, bool
- 数组:函数内修改不影响原数组(写时复制 COW)
- 对象重新赋值:
$param = new Foo() 不影响调用者
引用传递的情况
- 函数签名使用
&$param:显式引用传递,修改形参影响实参
- 全局变量通过
global 关键字(本质是引用)
- foreach 使用
&$v 时
⚠ 对象:传递「对象标识符」(Object Handle)
- 传递对象时,复制的是「对象标识符」(不是对象本身,也不是真正的引用)
- 函数内
$param->prop = 1 → 影响原始对象(两个标识符指向同一对象)
- 函数内
$param = new Foo() → 不影响原始变量
// ① 值传递(标量)
function modifyInt($n) {
$n = 100;
}
$a = 42;
modifyInt($a);
echo $a; // 42 ← 没变
// ② 引用传递(& 符号)
function modifyRef(&$n) {
$n = 100;
}
modifyRef($a);
echo $a; // 100 ← 改了
// ③ 数组 —— 写时复制(COW)
function modifyArray($arr) {
$arr[0] = 99; // 触发 COW,生成副本
}
$arr = [1, 2, 3];
modifyArray($arr);
print_r($arr); // [1, 2, 3] ← 没变
// ④ 对象:传递标识符副本
class Box { public $val = 1; }
function modifyProp($obj) {
$obj->val = 99; // ✅ 修改属性 —— 影响原对象
}
function reassignObj($obj) {
$obj = new Box(); // ❌ 重新赋值 —— 不影响原变量
}
$box = new Box();
modifyProp($box);
echo $box->val; // 99 ← 字段被改了
reassignObj($box);
echo $box->val; // 99 ← $box 本身没换
💡
Python 的机制叫做 "Pass by Object Reference"(对象引用传递),也可以理解为「值传递,传的是对象引用的副本」。关键在于对象是否 可变(mutable)。
不可变类型(Immutable)— 行为像值传递
int, float, complex, bool
str(字符串)
tuple(元组)
frozenset
- 函数内
x = 100 只是让局部变量指向新对象,原始变量不受影响
可变类型(Mutable)— 修改内部状态会影响原对象
list(列表)
dict(字典)
set(集合)
- 自定义
class 实例
- 通过引用修改内部字段/元素 → 影响原始对象
- 重新赋值形参 → 不影响原始变量
# ① 不可变类型 —— 函数内修改不影响外部
def modify_int(n):
n = 100 # 只是让 n 指向新对象 100
print(id(n)) # 地址变了
a = 42
print(id(a)) # 某个地址,如 140234567890
modify_int(a)
print(a) # 42 ← 没变
# ② 可变类型 list —— 修改元素影响原始
def modify_list(lst):
lst[0] = 99 # ✅ 修改元素 —— 影响原始列表
def reassign_list(lst):
lst = [9, 9, 9] # ❌ 重新赋值 —— 不影响原始变量
nums = [1, 2, 3]
modify_list(nums)
print(nums) # [99, 2, 3] ← 改了
reassign_list(nums)
print(nums) # [99, 2, 3] ← 没变
# ③ 字符串(不可变)—— += 创建新对象
def modify_str(s):
s += " world" # 创建新字符串对象
msg = "hello"
modify_str(msg)
print(msg) # hello ← 没变!
# ④ 自定义对象 —— 可变,修改属性影响原始
class Point:
def __init__(self, x): self.x = x
def modify_obj(p):
p.x = 999 # ✅ 修改属性 —— 影响原始对象
pt = Point(1)
modify_obj(pt)
print(pt.x) # 999 ← 改了
# ⑤ 验证:用 id() 查看对象地址
x = [1, 2]
print(id(x)) # 原始列表地址
def check_id(lst):
print(id(lst)) # 相同地址 —— 传的是引用的副本(同一对象)
check_id(x)
⚠️
Python 经典陷阱:默认参数用可变类型!def f(lst=[]) 中的 [] 只创建一次,多次调用共享同一个列表对象,导致行为异常。正确写法:def f(lst=None): if lst is None: lst = []
💡
Java 一切皆值传递(James Gosling 亲口确认)。基本类型传值的副本,对象传引用的副本。Java 没有引用传递,C++ 中的 & 引用传递在 Java 中不存在。
值传递(基本类型 — 8种)
byte, short, int, long
float, double
char
boolean
- 直接存储在栈上,传递时完整复制
传递引用副本(所有引用类型)
- 所有
Object 子类(String, ArrayList, 自定义类...)
- 数组(
int[], String[] 等)
- 传递的是堆对象的引用(地址)的副本
- 可以修改对象的字段,不能让调用者的变量指向新对象
⚠ String 特殊:不可变 + 字符串常量池
- String 是引用类型,但内容不可变(final char[])
- 函数内
s = s + "world" 创建新对象,原始变量不变
- 需要在函数内改变字符串并返回,应使用
StringBuilder 或返回新值
public class PassDemo {
// ① 基本类型 —— 值传递,完整复制
static void modifyInt(int n) {
n = 100; // 只修改局部副本
}
// ② 对象 —— 传引用副本,可修改字段
static void modifyField(int[] arr) {
arr[0] = 99; // ✅ 修改数组元素 —— 影响原数组
}
// ③ 重新赋值形参 —— 不影响调用者变量
static void reassign(int[] arr) {
arr = new int[]{9, 9, 9}; // ❌ 只改了局部引用副本
}
// ④ 自定义对象
static class Point { int x; Point(int x) { this.x = x; } }
static void modifyPoint(Point p) {
p.x = 999; // ✅ 影响原始对象
}
static void reassignPoint(Point p) {
p = new Point(0); // ❌ 不影响调用者
}
// ⑤ String —— 不可变类型
static void modifyString(String s) {
s = s + " world"; // 创建新对象,原始不变
}
public static void main(String[] args) {
int a = 42;
modifyInt(a);
System.out.println(a); // 42 ← 没变
int[] arr = {1, 2, 3};
modifyField(arr);
System.out.println(arr[0]); // 99 ← 改了
reassign(arr);
System.out.println(arr[0]); // 99 ← 没换(reassign无效)
Point pt = new Point(1);
modifyPoint(pt);
System.out.println(pt.x); // 999 ← 改了
reassignPoint(pt);
System.out.println(pt.x); // 999 ← 没变
String msg = "hello";
modifyString(msg);
System.out.println(msg); // hello ← 没变
}
}
✅
Java 口诀:基本类型 → 传值,对象类型 → 传引用副本。能改字段,不能换对象。String 虽是对象,但不可变,行为等同基本类型。