Skip to content

结构体

作者: ryan 发布于: 9/5/2025 更新于: 9/5/2025 字数: 0 字 阅读: 0 分钟

Go的结构体在数据封装层面类似“类”,但通过组合替代继承接口实现多态,以及首字母大小写控制封装,构建了一套去中心化、高内聚低耦合的轻量OOP模型。它并非“不完整的类”,而是对OOP范式的重新设计,更符合现代高并发与模块化开发需求。

结构体定义

结构体是自定义类型

go
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 声明

零值可用

go
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.字面量初始化

go
u2 := User{}  //字段为零值  
fmt.Printf("%#v\n", u2)

3.字面量初始化字段赋值

通过 field: value 语法直接为结构体的字段赋值的简洁方式。

go
u3 := User{id: 100}  
fmt.Printf("%#v\n", u3)
go
u4 := User{  
    id: 102,score: 88.5,  
    addr: "beijing",name: "jack",  
} //名称对应无所谓顺序  
  
fmt.Printf("%#v\n", u4)
go
u5 := User{103,"ryan","beijing",92.5} //无字段名称必须按照顺序给出全部字段值  
fmt.Printf("%#v\n", u5)

可见性

Go包的顶层代码中,首字母大写的标识符,跨package包可见(导出),否则只能本包内可见 导出的结构体,package内外皆可见,同时,导出的结构体中的成员(属性、方法)要在包外也可见,则也需首字母大写

属性访问

可以使用字段名称访问

go
u1 = User{110,"mm","beijing",82.5}  
fmt.Println(u1.id,u1.name,u1.score)

属性修改

通过字段来修改

go
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 方法实现了更自然的面向对象调用风格,使代码更符合“对象行为”的直觉表达,而普通函数更适合通用工具操作。

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())  
}

结构体指针

go
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).xp3.x 的区别

(*p3).x 是通过地址取值找到结构体,通过结构体在取其值

p3.x 通过指针访问 x


go
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)  
  
}

输出:

go
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

可以看出,结构体是非引用类型,使用的是值拷贝。传参或返回值如果使用结构体实例,将产生很多副本。 如何避免过多副本,如何保证函数内外使用的是同一个结构体实例呢?使用指针。


结构体的值传递与指针传递的区别

go
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
}
  • p3p4p5 均指向 p1,任一指针修改字段都会改变 p1 的值。

指针就是大整数 地址结构体的指针怎么获取?

  1. 先创建实例,取地址
go
var p1 Point   &p1
var p1 = Point{}  &p1

//主要为了填充结构体中的值
var p1 = Point{1,2}  &p1
  1. 用new,定义出该结构体的零值实例并返回该实例的指针new(Point)

匿名结构体

在Go语言中,匿名结构体是一种无需通过type预先定义结构体名称,而是直接在变量声明或初始化时定义结构体类型的方式。其核心特点是即用即抛,适用于临时性、局部性的数据聚合场景。

可以使用 varconst:= 来定义匿名结构体。

使用type定义结构体的标识符,可以反复定义其结构体实例

go
//定义一个结构体类型,指代它的标识符名称为Point  
type Point struct {  
    x, y int  
}

没有使用 type 显式命名类型,变量 point 的类型即为这个匿名的结构体,其类型标识为 struct{ x, y int },匿名结构体是一次性的无法在其他地方复用该类型名称。

go
//使用var定义一个变量point ,后面是类型,这个类型没有标识符名称,只有结构体本身  
var point struct{  
    x, y int  
} // 定义Point是后面匿名结构体类型的,用零值

fmt.Printf("%#v\n", point) // 得到的是一个结构体实例

//匿名结构体是一次性的。
go
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个实例。

匿名成员

有时候结构体中属性名可以省略

go
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

结构体字段多且有大量可选参数

go
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的返回值使用了值拷贝,增加了内存开销,习惯上返回值会采用指针类型,避免实例的拷贝。

go
func newAnimal(name string, age int) *Animal {  
  
a := Animal{name,age}  
return &a

}

在一个结构体几百个参数的情况下,提供一个只需要几个参数就可以实例化返回一个实例的函数很方便。 提供缺省值

在构造函数中直接设置默认值

go

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)  
}

通过上述方法,即使结构体有几百个字段,也可以优雅地实现“部分参数实例化 + 缺省值”的需求。

父子关系构造

动物类包括猫类,猫属于猫类,猫也属于动物类,某动物一定是动物类,但不能说某动物一定是猫类。

抽取共同的特征形成一类抽象概念

go

  
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

go
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结构体组合

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 子类天然可赋值给父类类型(里氏替换原则)。

java
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)有显著区别。

go
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
修改原始数据✅ 是(直接操作原对象)❌ 否(操作副本)
调用灵活性可用值或指针调用(编译器自动转换)同上
性能开销低(仅复制指针)高(需复制整个结构体)
方法内部作用域影响原对象不影响原对象

示例对比

go
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
}
go
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())  
}
go
//执行结果
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

这里为什么无论是实例还是指针调用,结果都一样?

go
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)。
go
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)  
  
}
go
//运行结果
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 深拷贝,往往会递归复制一定深度


核心区别与概念

特性浅拷贝深拷贝
复制深度仅复制对象的第一层属性(基本类型值复制,引用类型复制地址)递归复制所有层级,包括嵌套对象和数组
内存独立性新旧对象共享引用类型属性的内存地址新旧对象完全独立,不共享任何内存地址
修改影响修改引用类型属性会影响原对象修改任何属性均不影响原对象
适用对象结构简单或嵌套层级固定的对象嵌套结构复杂或需完全隔离的对象

注意,深浅拷贝说的是拷贝过程中是否发生递归拷贝,也就是说如果某个值是一个地址,是只复制这个地址 ,还是复制地址指向的内容。 值拷贝是深拷贝,地址拷贝是浅拷贝,这种说法是错误的。因为地址拷贝只是拷贝了地址,因此本质上来讲也是值拷贝。

Shallow-Copy.png

Deep-Copy.png


值拷贝和深浅拷贝

值拷贝只针对基本数据类型(如数字、字符串),直接复制值本身。深浅拷贝针对引用类型(对象、数组),区别在于是否跟进复制底层结构,而非仅值本身

Go语言中,引用类型实际上拷贝的是标头值,这也是值拷贝,并没有通过标头值中对底层数据结构的指针指向的内容进行复制,这就是浅拷贝。非引用类型的复制就是值拷贝,也就是再造一个副本,这也是浅拷贝。因为你不能说对一个整数值在内存中复制出一个副本,就是深的拷贝。 像整数类型这样的基本类型就是一个单独的值,没法深入拷贝,根本没法去讲深入的事儿。

简单讲,大家可以用拷贝文件是否对软链接跟进来理解。直接复制软链接就是浅拷贝,钻进软链接里面复制其内容就是深拷贝。

复杂数据结构,往往会有嵌套,有时嵌套很深,如果都采用深拷贝,那代价很高,所以,浅拷贝才是语言普遍采用的方案。