golang结构体

1. 类型定义

在Go语言中,类型别名和类型定义有一些区别。下面是它们的主要区别:

  1. 类型定义(Type Definition)

    • 通过关键字 type 可以创建一个新的类型,并给它取一个新的名字。
    • 新创建的类型与原始类型是不同的,不能直接赋值或比较。
    • 示例:
    type MyInt int
    
  2. 类型别名(Type Alias)

    • 通过关键字 type 可以为已有的类型创建一个别名,这个别名可以和原类型互换使用。
    • 类型别名在概念上只是为已有类型取了个别名,它们实际上是同一类型。
    • 示例:
    type MyString = string
    
  3. 互换性

    • 类型定义创建的新类型和原类型是不可互换的,需要进行显式类型转换。
    • 类型别名创建的别名和原类型是可互换的,可以直接赋值或进行比较。

下面是一个简单的示例,说明了类型定义和类型别名的使用:

package main

import "fmt"

type MyInt int

type MyString = string

func main() {
    var i MyInt = 42
    var j int = 100

    // MyInt类型和int类型不可直接互换
    // fmt.Println(i + j) // 编译错误

    var s MyString = "Hello"
    var t string = "World"

    // MyString类型和string类型可直接互换
    fmt.Println(s + t) // 输出:HelloWorld
}

总的来说,

  • 类型定义创建了一个新的类型,而类型别名只是为已有类型创建了一个别名。
  • 类型定义的新类型和原类型不可互换,而类型别名则是可互换的。

下面是使用表格形式展示类型别名和类型定义的异同:

特性 类型定义(Type Definition) 类型别名(Type Alias)
关键字 type type
创建新类型 否(与原类型是同一类型)
是否可互换 是(可直接赋值或进行比较)
示例 type MyInt int type MyString = string
互换性示例 需要显式类型转换 可直接赋值或进行比较

2. 结构体

在Go语言中,结构体(Struct)是一种用户定义的数据类型,用于组合多个不同类型的字段,每个字段可以是任意的基本类型或其他结构体类型。结构体是一种复合数据类型,用于组织和存储相关的数据。

以下是结构体的一些基本概念和用法:

1. 定义结构体

// 定义一个结构体
type Person struct {
FirstName string
LastName string
Age int
}

// 创建结构体变量
var person1 Person

2. 初始化结构体

// 初始化结构体的字段
person1 := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}

3. 访问结构体字段

// 访问结构体字段
fmt.Println(person1.FirstName) // 输出: John
fmt.Println(person1.LastName) // 输出: Doe
fmt.Println(person1.Age) // 输出: 30

4. 结构体嵌套

// 结构体嵌套
type Address struct {
City string
State string
}

type Employee struct {
Person // 匿名字段,结构体嵌套
Address // 匿名字段,结构体嵌套
Position string
}

// 创建结构体变量
employee1 := Employee{
Person: Person{
FirstName: "Alice",
LastName: "Smith",
Age: 25,
},
Address: Address{
City: "New York",
State: "NY",
},
Position: "Software Engineer",
}

5. 方法与结构体

在Go语言中,可以为结构体定义方法:

// 在结构体上定义方法
func (p Person) FullName() string {
return p.FirstName + " " + p.LastName
}

// 调用结构体方法
fullName := person1.FullName()
fmt.Println(fullName) // 输出: John Doe

6. 结构体的匿名字段

// 结构体的匿名字段
type Circle struct {
Radius float64
}

type Cylinder struct {
Circle // 匿名字段
Height float64
}

// 创建结构体变量
cylinder := Cylinder{
Circle: Circle{
Radius: 5.0,
},
Height: 10.0,
}
特性 Go语言中的结构体 C语言中的结构体 Java中的类
语法 使用 type 关键字定义结构体 使用 struct 关键字定义结构体 使用 class 关键字定义类
字段定义 字段名在前,类型在后 类型在前,字段名在后 类型在前,字段名在后
实例化 使用结构体字面量或 new 关键字 使用结构体字面量或 malloc 函数 使用 new 关键字或直接调用构造方法
匿名字段 支持匿名字段,可实现嵌套结构体 不支持直接的匿名字段,通过结构体嵌套实现 支持成员变量的直接定义和初始化
方法 使用关键字 func 定义方法,与结构体关联 不支持方法,通过函数操作结构体 使用关键字 public, private, protected 定义成员变量,通过方法操作
继承 不支持经典的继承,可以通过匿名字段模拟 不支持继承,通过结构体嵌套实现 支持单继承,可以通过 extends 关键字实现
多态 不支持经典的多态,可以通过接口实现 不支持多态 支持多态,通过方法重写和接口实现
构造函数 通过定义返回结构体的函数实现 无构造函数的概念 使用构造方法初始化对象
销毁 由垃圾收集器负责管理内存 使用 free 函数手动释放内存 由垃圾收集器负责管理内存
指针 不需要显式使用指针 可以使用指针,通过 * 操作符 不需要显式使用指针,内部通过引用操作

在Go语言中,结构体(struct)是一种用户定义的数据类型,用于组织和存储多个字段。以下是一个简单的结构体的定义和使用的例子:

package main

import "fmt"

// 定义结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 创建结构体实例
    person1 := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 访问结构体字段
    fmt.Println("First Name:", person1.FirstName)
    fmt.Println("Last Name:", person1.LastName)
    fmt.Println("Age:", person1.Age)

    // 修改结构体字段
    person1.Age = 31
    fmt.Println("Updated Age:", person1.Age)

    // 使用结构体字面量创建实例
    person2 := Person{"Jane", "Doe", 28}

    // 打印结构体实例
    fmt.Println("Person 2:", person2)

    // 结构体嵌套
    type Address struct {
        City  string
        State string
    }

    type Contact struct {
        Email   string
        Address Address
    }

    // 创建嵌套结构体实例
    contact := Contact{
        Email: "jane@example.com",
        Address: Address{
            City:  "New York",
            State: "NY",
        },
    }

    // 访问嵌套结构体字段
    fmt.Println("Email:", contact.Email)
    fmt.Println("City:", contact.Address.City)
    fmt.Println("State:", contact.Address.State)
}

使用指针形式给结构体中字段赋值

在Go语言中,可以使用new关键字创建一个结构体实例,并返回一个指向该结构体实例的指针。通过这个指针,可以直接访问和修改结构体的字段。以下是一个例子:

package main

import "fmt"

// 定义结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 使用 new 创建结构体实例的指针
    personPtr := new(Person)

    // 直接通过指针访问和修改结构体字段
    (*personPtr).FirstName = "John"
    (*personPtr).LastName = "Doe"
    (*personPtr).Age = 30

    // 或者可以使用隐式的指针间接引用,Go语言会自动转换
    personPtr.FirstName = "Jane"
    personPtr.LastName = "Doe"
    personPtr.Age = 28

    // 打印结构体实例
    fmt.Println("Person:", *personPtr)
}

结构体内存布局

在Go语言中,结构体的内存布局是按照字段的声明顺序来分配的,但是会考虑字段的对齐规则,以提高内存访问的效率。结构体的对齐规则是将每个字段的起始地址调整到它的类型大小的整数倍。

以下是一个简单的例子:

package main

import (
	"fmt"
	"unsafe"
)

type Point struct {
	X int32
	Y int32
}

type Rectangle struct {
	TopLeft     Point
	BottomRight Point
}

func main() {
	rect := Rectangle{
		TopLeft:     Point{10, 20},
		BottomRight: Point{30, 40},
	}

	// 打印结构体内存布局信息
	fmt.Printf("Size of Point: %d bytes\n", unsafe.Sizeof(Point{}))
	fmt.Printf("Size of Rectangle: %d bytes\n", unsafe.Sizeof(rect))

	// 打印结构体字段的地址
	fmt.Printf("TopLeft X address: %p\n", &rect.TopLeft.X)
	fmt.Printf("TopLeft Y address: %p\n", &rect.TopLeft.Y)
	fmt.Printf("BottomRight X address: %p\n", &rect.BottomRight.X)
	fmt.Printf("BottomRight Y address: %p\n", &rect.BottomRight.Y)
}

在上面的例子中,我们定义了一个Point结构体和一个包含两个Point结构体的Rectangle结构体。我们使用unsafe.Sizeof来获取结构体的大小,并使用%p格式化符打印字段的地址。

在结构体Point中,两个int32类型的字段 XY 是按照声明的顺序依次排列的。在结构体Rectangle中,TopLeftBottomRight是两个Point类型的字段,它们会被按照声明顺序排列。最终,整个Rectangle结构体的大小是两个Point结构体的大小之和。

请注意,Go语言的结构体对齐规则可能因编译器和目标平台而异。

字节对齐

+-----------------------------------------+
| Field1 (int32) | Padding (4 bytes)      |  --> 8 bytes aligned
+-----------------------------------------+
| Field2 (bool)  | Padding (4 bytes)      |  --> 8 byte aligned
+-----------------------------------------+
| Field3 (float64)                        |  --> 8 bytes aligned
+-----------------------------------------+

image

image

Go 在编译的时候会按照一定的规则自动进行内存对齐。之所以这么设计是为了减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。如果不进行内存对齐的话,很可能就会增加CPU访问内存的次数。例如下图中CPU想要获取b1.y字段的值可能就需要两次总线周期。

Golang 进行内存对齐主要有以下几个原因:

1. 提高性能:
CPU 访问内存时,通常是以字长为单位进行访问的。如果内存没有对齐,则 CPU 可能需要进行多次访问才能读取或写入数据,这会降低性能。
对齐的内存可以提高 CPU 的缓存命中率,因为缓存通常也是以字长为单位进行存储的。
2. 提高安全性:
未对齐的内存可能会导致数据溢出,从而导致安全漏洞。
3. 提高代码移植性:
不同的平台对内存对齐的要求可能不同。对齐的内存可以保证代码在不同平台上都能正确运行。
Golang 中的内存对齐规则:

  • 每个类型的变量都有一个默认的对齐值。
  • 结构体的对齐值为其成员中最大类型的对齐值。
  • 数组的对齐值为其元素类型的对齐值。

如何控制内存对齐:

  • 使用 unsafe 包中的 Alignof 函数获取类型的对齐值。
  • 使用 unsafe 包中的 Offsetof 函数获取结构体成员的偏移量。
  • 使用 reflect 包中的 StructOf 函数获取结构体的类型信息。
    示例:
package main

import (
    "fmt"
    "unsafe"
)

type MyStruct struct {
    A int8
    B int64
}

func main() {
    // 获取 int8 的对齐值
    fmt.Println(unsafe.Alignof(int8(0))) // 输出: 1
	
    // 获取 MyStruct 的对齐值
    fmt.Println(unsafe.Alignof(MyStruct{})) // 输出: 8
	
    // 获取 MyStruct 中 B 的偏移量
    fmt.Println(unsafe.Offsetof(MyStruct{}.B)) // 输出: 8
}

结构体示例

package main

import "fmt"

type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "小王子", age: 18},
		{name: "娜扎", age: 23},
		{name: "大王八", age: 9000},
	}

	for _, stu := range stus {
		m[stu.name] = &stu
	}
	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}

上述代码的输出结果是:

大王八 => 大王八
小王子 => 大王八
娜扎 => 大王八

这样的输出结果可能与代码预期不符。这是因为在循环中,将 &stu 存储到 map 中,而 stu 是在每次循环迭代中重新分配的。由于 map 存储的是指向 stu 的指针,最终 map 中存储的都是最后一次迭代的 stu 的地址。

为了解决这个问题,可以在循环中创建一个局部变量,将其地址存储到 map 中,如下所示:

package main

import "fmt"

type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "小王子", age: 18},
		{name: "娜扎", age: 23},
		{name: "大王八", age: 9000},
	}

	for _, stu := range stus {
		// 创建局部变量 localStu
		localStu := stu
		m[stu.name] = &localStu
	}
	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}

这样就能够正确输出预期的结果:

小王子 => 小王子
娜扎 => 娜扎
大王八 => 大王八

构造函数 + 方法

//Person 结构体
type Person struct {
	name string
	age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

//Dream Person做梦的方法
func (p Person) Dream() {
	fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
	p1 := NewPerson("小王子", 25)
	p1.Dream()
}

在Go中,方法是与特定类型关联的函数。方法允许在用户自定义的类型上定义行为,并允许操作该类型的实例。方法与函数类似,但有一个额外的接收者参数。

下面是关于Go方法的一些重要概念:

方法定义

方法定义的一般语法如下:

func (receiver ReceiverType) methodName(parameters) returnType {
    // 方法实现
}
  • receiver 是一个参数,表示方法的接收者,可以是值接收者(对实例的拷贝)或指针接收者(对实例的引用)。
  • ReceiverType 是接收者的类型。
  • methodName 是方法的名称。
  • parameters 是方法的参数。
  • returnType 是方法的返回类型。

值接收者与指针接收者

  • 值接收者(Value Receiver):方法的接收者是接收者类型的一个副本,对副本的修改不会影响原始值。
  • 指针接收者(Pointer Receiver):方法的接收者是接收者类型的一个指针,对指针指向的值的修改会影响原始值。

方法调用

方法调用的一般语法如下:

instance.methodName(parameters)

示例

package main

import "fmt"

type Rectangle struct {
    width, height float64
}

// 值接收者方法
func (r Rectangle) area() float64 {
    return r.width * r.height
}

// 指针接收者方法
func (r *Rectangle) scale(factor float64) {
    r.width *= factor
    r.height *= factor
}

func main() {
    rect := Rectangle{width: 10, height: 5}

    // 调用值接收者方法
    fmt.Println("Area:", rect.area())

    // 调用指针接收者方法
    rect.scale(2)
    fmt.Println("Scaled Width:", rect.width)
    fmt.Println("Scaled Height:", rect.height)
}

在上面的例子中,Rectangle 类型有两个方法,area 是值接收者方法,scale 是指针接收者方法。主函数演示了如何创建 Rectangle 实例并调用这两个方法。

使用指针接收者时机

在 Go 中,选择使用值接收者还是指针接收者取决于你的设计需求和对数据的处理方式。以下是一些建议:

  1. 需要修改接收者实例的值时: 如果方法需要修改接收者实例的值,应该使用指针接收者。因为值接收者是接收实例的一个副本,对该副本的修改不会影响原始实例。

    func (r *Rectangle) scale(factor float64) {
        r.width *= factor
        r.height *= factor
    }
    
  2. 避免拷贝大对象: 如果接收者类型是一个大的结构体或包含大量数据的类型,使用指针接收者可以避免在方法调用时产生整个对象的拷贝。

  3. 实现接口时: 如果一个类型实现了某个接口,而该接口的方法使用了指针接收者,那么在该类型的实现中,相应的方法也需要使用指针接收者。

  4. 一致性: 对于一个类型的方法,最好在所有方法中一致地使用值接收者或指针接收者,以确保一致性和可读性。

  5. nil 接收者: 如果一个方法在逻辑上不依赖于实例,可以考虑使用 nil 指针接收者。这样可以避免创建一个实际上不会使用的实例。

需要注意的是,使用指针接收者可能导致 nil 指针引发运行时错误。在调用一个使用指针接收者的方法时,确保接收者不是 nil。如果对 nil 指针调用值接收者方法,编译器会自动解引用并将其视为对 nil 指针的调用。

综上所述,选择值接收者还是指针接收者主要取决于方法对实例的修改需求以及性能方面的考虑。

使用指针类型做为类型方法的接收者

当一个类型的方法使用指针类型接收者时,该类型必须是一个指针类型,而不是该类型的值。以下是一个使用指针类型接收者的接口实现的示例:

package main

import "fmt"

// 定义一个接口
type Shape interface {
    Area() float64
}

// 定义一个矩形类型
type Rectangle struct {
    Width  float64
    Height float64
}

// 为 *Rectangle 实现 Shape 接口的 Area 方法
func (r *Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    // 创建一个矩形指针实例
    rect := &Rectangle{Width: 5, Height: 10}

    // 将矩形指针实例赋值给 Shape 接口类型的变量
    var shape Shape = rect

    // 调用接口方法
    area := shape.Area()

    fmt.Printf("矩形的面积为: %.2f\n", area)
}

在上述例子中,Rectangle 类型的 Area 方法使用指针类型接收者(*Rectangle)。因此,我们创建了一个 Rectangle 类型的指针实例,并将其赋值给 Shape 接口类型的变量。最后,通过接口调用 Area 方法,实现了对矩形的面积计算。使用指针类型接收者的好处是可以在方法中修改接收者的值。

posted @ 2024-03-10 14:40  zhongweiLeex  阅读(49)  评论(0编辑  收藏  举报