结构体
作者: ryan 发布于: 9/5/2025 更新于: 9/5/2025 字数: 0 字 阅读: 0 分钟
Go的结构体在数据封装层面类似“类”,但通过组合替代继承、接口实现多态,以及首字母大小写控制封装,构建了一套去中心化、高内聚低耦合的轻量OOP模型。它并非“不完整的类”,而是对OOP范式的重新设计,更符合现代高并发与模块化开发需求。
结构体定义
结构体是自定义类型
type User struct {
name,addr string
age int
id int
}
User是一个标识符(标识符的本质就是一个指代) 真正的类型定义是struct
{}
初始化
类型和实例
int、string 、bool结构体都是类型
1
是 int的一个实例,它是int类型
""
是string的一个实例,它是string类型 [3]int
是类型,[3]int{}
字面量表达一个数组,它是[3]int
类型
结构体实例化时通过字面量初始化,如 struct{}{}
创建空结构体实例。
1.var 声明
零值可用
type User struct {
id int
name, addr string
score float32
}
func main() {
var u1 User //这种方式声明结构体变量很方便,所有字段都是零值
fmt.Printf("%T\n", u1)
fmt.Println(u1)
fmt.Printf("%v\n", u1) //默认打印
fmt.Printf("%+v\n",u1) //加上字段打印
fmt.Printf("%#v\n",u1) //加上更多信息打印
}
2.字面量初始化
u2 := User{} //字段为零值
fmt.Printf("%#v\n", u2)
3.字面量初始化字段赋值
通过 field: value
语法直接为结构体的字段赋值的简洁方式。
u3 := User{id: 100}
fmt.Printf("%#v\n", u3)
u4 := User{
id: 102,score: 88.5,
addr: "beijing",name: "jack",
} //名称对应无所谓顺序
fmt.Printf("%#v\n", u4)
u5 := User{103,"ryan","beijing",92.5} //无字段名称必须按照顺序给出全部字段值
fmt.Printf("%#v\n", u5)
可见性
Go包的顶层代码中,首字母大写的标识符,跨package包可见(导出),否则只能本包内可见 导出的结构体,package内外皆可见,同时,导出的结构体中的成员(属性、方法)要在包外也可见,则也需首字母大写
属性访问
可以使用字段名称访问
u1 = User{110,"mm","beijing",82.5}
fmt.Println(u1.id,u1.name,u1.score)
属性修改
通过字段来修改
u1 = User{110, "mm", "beijing", 82.5}
fmt.Println(u1.id, u1.name, u1.score)
u1.name = "Tom"
u1.score = 100
fmt.Println(u1)
成员方法
结构体的方法、属性,成为成员。方法也称为成员方法,属性也称为成员属性。
普通函数与结构体方法虽最终目标一致(操作数据),但通过 接收者机制,Go 方法实现了更自然的面向对象调用风格,使代码更符合“对象行为”的直觉表达,而普通函数更适合通用工具操作。
package main
import "fmt"
type User struct {
id int
name, addr string
score float32
}
// 一个普通的函数 ,独立的全局函数,无状态关联。
func getName(u User) string {
return u.name
}
// 结构体方法,本质上还是函数
// u receiver 接收者
//(u User)声明该方法关联到 User 类型,u是接收者的副本(值接收者)
func (u User) getName() string {
return u.name
}
func main() {
var u5 = User{111, "ben", "nanjing", 98.5}
fmt.Printf("%T %[1]v\n", u5)
fmt.Println(u5.name, getName(u5))
fmt.Println(u5.name)
fmt.Println(u5.getName())
}
结构体指针
package main
import "fmt"
type Point struct {
x, y int
}
func main() {
var p1 = Point{1, 2} //实例
fmt.Printf("%T %[1]v\n", p1) //Point,{1,2}
var p2 = &Point{3, 4} //指针
fmt.Printf("%T %[1]v\n", p2) //*Point,&{3,4}
var p3 = new(Point) //new 实例化一个结构体并返回
fmt.Printf("%T %[1]v\n", p3) // *Point,&{0,0}
fmt.Println("~~~~~~~~~~~~~~~~~~~~~")
//通过实例修改属性
p1.x = 100
fmt.Printf("%T %[1]v\n", p1) //Point,{100,2}
//通过指针修改属性
p2.x = 200
p3.x = 300
fmt.Printf("%T %[1]v\n", p2) //*Point, &{200,4}
fmt.Printf("%T %[1]v\n", p3) //*Point, &{300,0}
// p3.x中,是 -> 的语法糖,更方便使用,等价于(*p3.x)
fmt.Print(*p3,(*p3).x) //{300 0} 300
}
go 语言为我们提供的语法糖通过指针直接就能修改结构体,不需要 c++ 的 p2 ->x
(*p3).x
与 p3.x
的区别
(*p3).x
是通过地址取值找到结构体,通过结构体在取其值
p3.x
通过指针访问 x
package main
import "fmt"
type Point struct {
x, y int
}
func test(p Point) Point {
fmt.Printf("4 %+v %p\n", p, &p)
return p
}
func main() {
var p1 = Point{10, 20} //实例
fmt.Printf("1 %+v %p\n", p1, &p1)
p2 := p1
fmt.Printf("2 %+v %p\n", p2, &p2)
p3 := &p1
fmt.Printf("3 %+v %p\n", p3, p3)
p4 := test(p1)
fmt.Printf("5 %+v %p\n", p4, &p4)
}
输出:
1 {x:10 y:20} 0x188c0a8
2 {x:10 y:20} 0x188c0f0
3 &{x:10 y:20} 0x188c0a8
4 {x:10 y:20} 0x188c110
5 {x:10 y:20} 0x188c108
可以看出,结构体是非引用类型,使用的是值拷贝。传参或返回值如果使用结构体实例,将产生很多副本。 如何避免过多副本,如何保证函数内外使用的是同一个结构体实例呢?使用指针。
结构体的值传递与指针传递的区别
type Point struct {
x, y int
}
func test(p *Point) *Point {
p.x += 100 // 通过指针修改原始数据的字段
fmt.Printf("4 %+v %p\n", p, p) // 4 &{x:110 y:20} 0xc0000100c0
return p
}
func main() {
var p1 = Point{10, 20} // 创建 Point 实例 p1 (值类型)
fmt.Printf("1 %+v %p\n", p1, &p1) // 1 {x:10 y:20} 0xc0000100c0
p2 := p1 // 将 p1 的值复制到 p2(独立副本)
//p2 是 p1 的完整副本,地址不同,修改 p2 不影响p1。
fmt.Printf("2 %+v %p\n", p2, &p2) // 2 {x:10 y:20} 0xc0000100e0
p3 := &p1 // 创建指向 p1 的指针 p3
//p3 存储p1的内存地址(0xc0000100c0),操作 p3 会直接影响 p1
fmt.Printf("3 %+v %p\n", p3, p3) // 3 &{x:10 y:20} 0xc0000100c0
p4 := test(p3) // 传入 p3 指针(即 p1 的地址)
//test函数:通过指针 p 将 p1.x 从 10 改为 110。返回值 p4与 p3 指向同一地址0xc0000100c0
p4.x += 200 // p4 指向 p1,直接修改 p1.x
//- 通过 p4 修改 x 后,p1.x 变为 310(两者指向同一内存)。
fmt.Printf("5 %+v %p\n", p1, &p1) // 5 {x:310 y:20} 0xc0000100c0
fmt.Printf("6 %+v %p\n", p4, p4) // 6 &{x:310 y:20} 0xc0000100c0
p5 := p3 // p5 也指向 p1 的地址
p5.y = 400 // 修改 p5.y 即修改 p1.y
fmt.Printf("7 %+v %p\n", p1, &p1) // 7 {x:310 y:400} 0xc0000100c0
fmt.Printf("8 %+v %p\n", p5, p5) // 8 &{x:310 y:400} 0xc0000100c0
}
p3
、p4
、p5
均指向p1
,任一指针修改字段都会改变p1
的值。
指针就是大整数 地址结构体的指针怎么获取?
- 先创建实例,取地址
var p1 Point &p1
var p1 = Point{} &p1
//主要为了填充结构体中的值
var p1 = Point{1,2} &p1
- 用new,定义出该结构体的零值实例并返回该实例的指针
new(Point)
匿名结构体
在Go语言中,匿名结构体是一种无需通过type
预先定义结构体名称,而是直接在变量声明或初始化时定义结构体类型的方式。其核心特点是即用即抛,适用于临时性、局部性的数据聚合场景。
可以使用 var
、 const
、 :=
来定义匿名结构体。
使用type定义结构体的标识符,可以反复定义其结构体实例
//定义一个结构体类型,指代它的标识符名称为Point
type Point struct {
x, y int
}
没有使用 type
显式命名类型,变量 point
的类型即为这个匿名的结构体,其类型标识为 struct{ x, y int }
,匿名结构体是一次性的无法在其他地方复用该类型名称。
//使用var定义一个变量point ,后面是类型,这个类型没有标识符名称,只有结构体本身
var point struct{
x, y int
} // 定义Point是后面匿名结构体类型的,用零值
fmt.Printf("%#v\n", point) // 得到的是一个结构体实例
//匿名结构体是一次性的。
var message = struct {
id int
data string
}{1, "OK"} // 字面量初始化,不用零值,初始化
fmt.Printf("%#v\n", message)
student := struct {
id int
name string
}{1, "Tom"} // 短格式定义并初始化
fmt.Printf("%#v\n", student)
匿名结构体,只是为了快速方便地得到一个结构体实例,而不是使用结构体创建N个实例。
匿名成员
有时候结构体中属性名可以省略
package main
import "fmt"
type Point struct {
x int //x 是属性的标识符名称 “x” int // 没有名称,占用了一个位置放int数据 int int
bool
}
func main() {
var p1 = Point{4,5,true}
fmt.Println(p1.x,p1.int,p1.bool)
}
注意:结构体内同类型的只能有一个可以省略标识符名称
在实际的使用过程中慎重使用匿名成员,业务名没名字会给使用者带来困扰。
构造函数
在Go语言中,没有构造函数(constructor)的概念,不像一些面向对象的语言(如C++、Java)那样有专门的构造函数。
但是,我们通常通过定义一个函数来返回该类型的实例(有时是指针)来模拟构造函数的行为。这种函数通常以New
开头,后接类型名,所以也被称为“New函数”或者“工厂函数”。
构造函数 go 是没有的,使用者约定俗成的,提供一个返回该类型或指针的 普通函数。 名字建议: Newxxx 或 new xxx
结构体字段多且有大量可选参数
package constructor
import "fmt"
type Animal struct {
name string
age int
food string
// ... 其他几百个字段
}
// 普通的函数
func newAnimal(name string, age int) Animal {
a := new(Animal)
a.name = name
a.age = age
return a
}
func main() {
//缺少值
var a = Animal{"Jack", 20}
fmt.Println(a)
var b = newAnimal("Jack", 20)
fmt.Println(b, b.name, b.age)
}
上例中,NewAnimal的返回值使用了值拷贝,增加了内存开销,习惯上返回值会采用指针类型,避免实例的拷贝。
func newAnimal(name string, age int) *Animal {
a := Animal{name,age}
return &a
}
在一个结构体几百个参数的情况下,提供一个只需要几个参数就可以实例化返回一个实例的函数很方便。 提供缺省值
在构造函数中直接设置默认值
package main
import "fmt"
type Animal struct {
name string
age int
food string // 设置默认值
}
func newAnimal() *Animal {
return &Animal{
name: "Dog",
age: 6,
}
}
func main() {
fmt.Println(c)
fmt.Printf("%T\n", c)
}
通过上述方法,即使结构体有几百个字段,也可以优雅地实现“部分参数实例化 + 缺省值”的需求。
父子关系构造
动物类包括猫类,猫属于猫类,猫也属于动物类,某动物一定是动物类,但不能说某动物一定是猫类。
抽取共同的特征形成一类抽象概念
type Animal struct {
name string
age int
}
type Cat struct {
a Animal
color string
}
func NewAnimal(name string, age int) *Animal {
return &Animal{name: name, age: age}
}
func main() {
c := new(Cat)
fmt.Printf("%T %+[1]v\n", c)
c.a.name = "Dog"
c.a.age = 10
c.color = "red"
fmt.Printf("%T %+[1]v\n", c)
}
父结构体定义的属性就变成了子结构体的了,像继承一样
Animal
的字段(如Name
)被提升到Cat
结构体,可直接通过c.name
访问(无需c.Animal.name
)
type Cat struct {
//a Animal
Animal //匿名属性,结构体嵌套
color string
}
func NewAnimal(name string, age int) *Animal {
return &Animal{name: name, age: age}
}
func main() {
c := new(Cat)
fmt.Printf("%T %+[1]v\n", c)
//c.a.name = "Dog"
//c.a.age = 10
c.name = "Dog"
c.age = 10
c.color = "red"
fmt.Printf("%T %+[1]v\n", c)
与面向对象的继承有本质区别
嵌入类型(如Animal
)与外部类型(如Cat
)是完全独立的类型。 Cat
不是 Animal
,而是包含一个 Animal
。 类型检查时,Cat
无法直接赋值给 Animal
类型
Go结构体组合
type Animal struct{}
type Cat struct {
Animal // 匿名嵌入:组合而非继承
}
func HandleAnimal(a Animal) {}
func main() {
c := Cat{}
// HandleAnimal(c) // 编译错误:Cat 不是 Animal
}
通过匿名嵌入,Go语言以组合方式实现了"猫是一种动物"的逻辑关系,同时严格维护类型的独立性,这与面向对象的继承有本质区别。
面向对象继承:
子类(Cat
)是父类(Animal
)的类型扩展,Cat
即是 Animal
子类天然可赋值给父类类型(里氏替换原则)。
class Animal {}
class Cat extends Animal {}
public void handleAnimal(Animal a) {}
public static void main() {
Cat cat = new Cat();
handleAnimal(cat); // 合法:Cat 是 Animal
}
指针类型 receiver
在 Go 语言中,方法的接收者(Receiver) 是方法与类型关联的关键机制,其形式为 func (receiver Type) 方法名(参数) 返回值 { ... }
。其中 Type
可以是结构体或其他自定义类型,而 指针类型 Receiver(*T
) 与值类型 Receiver(T
)有显著区别。
package main
import "fmt"
type Point struct {
x, y int
}
// 普通函数
func getX(p Point) int {
fmt.Printf("2 %+v,%p\n", p, &p)
return p.x
}
// 方法与Point 建立了联系 ,变成了 Point 的成员方法
func (p Point) getX() int {
fmt.Printf("3 %+v,%p\n", p, &p)
return p.x
}
func main() {
var t = Point{4, 5}
fmt.Printf("1 %+v,%p\n", t, &t)
fmt.Println(getX(t))
fmt.Println(t.getX())
}
指针类型Receiver与值类型的区别
特性 | 指针 Receiver(*T ) | 值 Receiver(T ) |
---|---|---|
修改原始数据 | ✅ 是(直接操作原对象) | ❌ 否(操作副本) |
调用灵活性 | 可用值或指针调用(编译器自动转换) | 同上 |
性能开销 | 低(仅复制指针) | 高(需复制整个结构体) |
方法内部作用域 | 影响原对象 | 不影响原对象 |
示例对比
func (p Point) getX() int { // 值接收者
fmt.Printf("3 %+v, %p\n", p, &p) // 打印副本的值和地址
return p.x
}
func (p *Point) getY() int { // 指针接收者
fmt.Printf("4 %+v, %p\n", p, &p) // 打印指针值和指针变量的地址
return p.y
}
package main
import "fmt"
type Point struct {
x, y int
}
// receiver如果是实例,这里依然会有值拷贝
// 语法糖允许使用实例或指针调用该方法,但是由于receiver是实例,所以这里都会有值拷贝
func (p Point) getX() int {
fmt.Printf("3 %+v, %p\n", p, &p)
return p.x
}
// 指针还是值拷贝,只不过拷贝本身就是指针(地址)
func (p *Point) getY() int {
fmt.Printf("4 %+v, %p\n", p, p)
return p.y
}
func main() {
var t = Point{4, 5}
fmt.Printf("1 %+v, %p\n", t, &t)
fmt.Println(t.getX(), (&t).getX())
fmt.Println("~~~~~~~~~~~~~~~~~~~")
fmt.Println(t.getY(), (&t).getY())
}
//执行结果
1 {x:4 y:5}, 0x100a0a8 //初始
3 {x:4 y:5}, 0x100a0f0 //第一次值接收器值拷贝
3 {x:4 y:5}, 0x100a100 //第二次值接收器虽然传的是指针但是还是值拷贝
4 4
~~~~~~~~~~~~~~~~~~~
4 &{x:4 y:5}, 0x180a0a8 //指针接收器
4 &{x:4 y:5}, 0x180a0a8
5 5
这里为什么无论是实例还是指针调用,结果都一样?
t.getY() // 值类型变量调用指针接收者方法
(&t).getY() // 指针类型变量直接调用
在 Go 语言中,指针接收者方法的调用机制具有特殊的语法糖特性。 当通过值类型变量(t
)调用指针接收者方法(getY
)时,编译器会隐式将 t
转换为指针 &t
,等价于 (&t).getY()
无论调用方式是值还是指针,最终传入 getY
方法的接收者参数 p
始终是 &t
,即指向原始 t
的指针。
通过 receiver 形成了一个新的实例,修改里面不影响外面的实例
setX
(值接收器)
每次调用时都创建了接收器的副本,内存地址变化(如0x140a120
→0x140a140
)修改p.x
只影响副本,原结构体t
的x
字段未被修改(输出结果中t.x
始终为100
)。setY
(指针接收器)
直接操作原始结构体内存地址(始终为0x140a0a8
),修改p.y
生效(t.y
从1
变为8
)。
package main
import "fmt"
type Point struct {
x, y int
}
func (p Point) setX(v int) {
fmt.Printf("5 %+v, %p\n", p, &p)
p.x = v
fmt.Printf("6 %+v, %p\n", p, &p)
}
func (p *Point) setY(v int) {
fmt.Printf("7 %+v, %p\n", p, p)
p.y = v
fmt.Printf("8 %+v, %p\n", p, p)
}
func main() {
var t = Point{4, 5}
t.x = 100
t.setX(1000)
fmt.Printf("9 %+v, %p\n", t, &t)
(&t).setX(999)
t.y = 1
t.setY(8)
fmt.Printf("10 %+v, %p\n", t, &t)
}
//运行结果
1 {x:4 y:5}, 0x140a0a8
3 {x:4 y:5}, 0x140a0f0
3 {x:4 y:5}, 0x140a100
4 4
~~~~~~~~~~~~~~~~~~~
4 &{x:4 y:5}, 0x140a0a8
4 &{x:4 y:5}, 0x140a0a8
5 5
5 {x:100 y:5}, 0x140a120
6 {x:1000 y:5}, 0x140a120
9 {x:100 y:5}, 0x140a0a8
5 {x:100 y:5}, 0x140a140
6 {x:999 y:5}, 0x140a140
7 &{x:100 y:1}, 0x140a0a8
8 &{x:100 y:8}, 0x140a0a8
10 {x:100 y:8}, 0x140a0a8
选择指针还是值类型
选择指针还是值类型取决于需求: 修改原对象?→指针; 只读或小对象?→值类型。
指针类型 Receiver 是 Go 中实现“通过方法修改原数据”的核心机制,其本质是编译器将方法转为以 Receiver 为第一参数的函数。
深浅拷贝
shadow copy 影子拷贝,也叫浅拷贝。遇到引用类型数据,仅仅复制一个引用而已
deep copy 深拷贝,往往会递归复制一定深度
核心区别与概念
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
复制深度 | 仅复制对象的第一层属性(基本类型值复制,引用类型复制地址) | 递归复制所有层级,包括嵌套对象和数组 |
内存独立性 | 新旧对象共享引用类型属性的内存地址 | 新旧对象完全独立,不共享任何内存地址 |
修改影响 | 修改引用类型属性会影响原对象 | 修改任何属性均不影响原对象 |
适用对象 | 结构简单或嵌套层级固定的对象 | 嵌套结构复杂或需完全隔离的对象 |
注意,深浅拷贝说的是拷贝过程中是否发生递归拷贝,也就是说如果某个值是一个地址,是只复制这个地址 ,还是复制地址指向的内容。 值拷贝是深拷贝,地址拷贝是浅拷贝,这种说法是错误的。因为地址拷贝只是拷贝了地址,因此本质上来讲也是值拷贝。
值拷贝和深浅拷贝
值拷贝只针对基本数据类型(如数字、字符串),直接复制值本身。深浅拷贝针对引用类型(对象、数组),区别在于是否跟进复制底层结构,而非仅值本身
Go语言中,引用类型实际上拷贝的是标头值,这也是值拷贝,并没有通过标头值中对底层数据结构的指针指向的内容进行复制,这就是浅拷贝。非引用类型的复制就是值拷贝,也就是再造一个副本,这也是浅拷贝。因为你不能说对一个整数值在内存中复制出一个副本,就是深的拷贝。 像整数类型这样的基本类型就是一个单独的值,没法深入拷贝,根本没法去讲深入的事儿。
简单讲,大家可以用拷贝文件是否对软链接跟进来理解。直接复制软链接就是浅拷贝,钻进软链接里面复制其内容就是深拷贝。
复杂数据结构,往往会有嵌套,有时嵌套很深,如果都采用深拷贝,那代价很高,所以,浅拷贝才是语言普遍采用的方案。