【Go-7】面向对象编程

7. 面向对象编程

面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,通过将数据和行为封装在对象中,以提高代码的可重用性、可维护性和扩展性。虽然Go语言不像传统的OOP语言(如Java、C++)那样提供类和继承的概念,但它通过结构体、方法、接口以及组合等特性,充分支持面向对象的编程风格。本章将详细介绍Go语言中的面向对象编程特性,包括方法、接口、组合与继承等内容。通过丰富的示例和深入的解释,帮助你全面理解和应用这些特性。

7.1 Go 的面向对象特性

Go语言虽然没有类(Class)和继承(Inheritance)的概念,但它通过以下几个关键特性实现了面向对象编程的核心理念:

  • 结构体(Structs):用于定义具有多个字段的复合数据类型,类似于其他语言中的类。
  • 方法(Methods):可以为结构体类型定义方法,赋予结构体行为。
  • 接口(Interfaces):定义了一组方法签名,任何实现了这些方法的类型都满足该接口,实现了多态性。
  • 组合(Composition):通过结构体嵌套实现代码复用和类型扩展,代替传统的继承。
  • 多态(Polymorphism):通过接口实现不同类型的统一操作。
封装(Encapsulation)

封装是OOP的核心概念之一,通过封装,可以隐藏对象的内部实现细节,仅暴露必要的接口。Go通过导出(首字母大写)和未导出(首字母小写)的标识符实现封装。

示例:

package mainimport "fmt"// 定义结构体
type Person struct {Name string // 导出字段age  int    // 未导出字段
}// 定义方法访问未导出字段
func (p *Person) GetAge() int {return p.age
}func (p *Person) SetAge(a int) {if a >= 0 {p.age = a}
}func main() {p := Person{Name: "Alice"}// p.age = 30 // 编译错误: age 是未导出的字段p.SetAge(30)fmt.Printf("Name: %s, Age: %d\n", p.Name, p.GetAge()) // 输出: Name: Alice, Age: 30
}

输出:

Name: Alice, Age: 30

解释:

  • Person结构体中,Name字段是导出的,可以在包外访问,而age字段是未导出的,只能在包内访问。
  • 通过GetAgeSetAge方法,控制对age字段的访问和修改,实现了封装。
方法(Methods)

方法是与特定类型关联的函数,赋予类型特定的行为。在Go中,方法的定义与函数类似,只是在函数名前添加接收者(Receiver)部分。

基本语法:

func (receiver Type) MethodName(parameters) returnTypes {// 方法体
}

示例:

package mainimport "fmt"// 定义结构体
type Rectangle struct {Width, Height float64
}// 定义方法计算面积
func (r Rectangle) Area() float64 {return r.Width * r.Height
}// 定义方法计算周长
func (r Rectangle) Perimeter() float64 {return 2 * (r.Width + r.Height)
}func main() {rect := Rectangle{Width: 10, Height: 5}fmt.Printf("面积: %.2f\n", rect.Area())         // 输出: 面积: 50.00fmt.Printf("周长: %.2f\n", rect.Perimeter())   // 输出: 周长: 30.00
}

输出:

面积: 50.00
周长: 30.00

解释:

  • Rectangle结构体定义了矩形的宽度和高度。
  • AreaPerimeter方法分别计算矩形的面积和周长。

指针接收者与值接收者:

方法的接收者可以是值类型或指针类型。选择哪种接收者取决于方法是否需要修改接收者的字段,以及是否希望避免大结构体的复制。

示例:

package mainimport "fmt"// 定义结构体
type Counter struct {count int
}// 值接收者方法
func (c Counter) IncrementValue() {c.count++fmt.Println("Inside IncrementValue:", c.count)
}// 指针接收者方法
func (c *Counter) IncrementPointer() {c.count++fmt.Println("Inside IncrementPointer:", c.count)
}func main() {c := Counter{count: 10}c.IncrementValue() // 修改的是副本fmt.Println("After IncrementValue:", c.count) // 输出: 10c.IncrementPointer() // 修改的是原始值fmt.Println("After IncrementPointer:", c.count) // 输出: 11// 使用指针变量调用方法cp := &ccp.IncrementPointer()fmt.Println("After cp.IncrementPointer:", c.count) // 输出: 12
}

输出:

Inside IncrementValue: 11
After IncrementValue: 10
Inside IncrementPointer: 11
After IncrementPointer: 11
Inside IncrementPointer: 12
After cp.IncrementPointer: 12

解释:

  • IncrementValue方法接收者为值类型,只修改方法内部的副本,不影响原始结构体。
  • IncrementPointer方法接收者为指针类型,修改的是原始结构体的字段。

注意事项:

  • 一致性:建议为同一类型的方法统一使用值接收者或指针接收者,避免混淆。
  • 性能:对于大型结构体,使用指针接收者可以避免复制,提高性能。
  • 可修改性:使用指针接收者可以在方法中修改接收者的字段。

7.2 方法

方法是与特定类型关联的函数,赋予类型特定的行为。Go语言中的方法可以定义在结构体类型上,使得结构体不仅具有数据,还具有行为。

方法的定义与调用

定义方法:

方法的定义与函数类似,不同之处在于方法有一个接收者(Receiver),用于指定该方法属于哪个类型。

基本语法:

func (receiver Type) MethodName(parameters) returnTypes {// 方法体
}
  • receiver:接收者,通常是结构体类型的实例,可以是值类型或指针类型。
  • Type:接收者的类型。
  • MethodName:方法名称。
  • parameters:方法的参数列表。
  • returnTypes:方法的返回值类型。

示例:

package mainimport "fmt"// 定义结构体
type Circle struct {Radius float64
}// 定义方法计算面积
func (c Circle) Area() float64 {return 3.14159 * c.Radius * c.Radius
}// 定义方法计算周长
func (c Circle) Circumference() float64 {return 2 * 3.14159 * c.Radius
}func main() {circle := Circle{Radius: 5}fmt.Printf("面积: %.2f\n", circle.Area())               // 输出: 面积: 78.54fmt.Printf("周长: %.2f\n", circle.Circumference())     // 输出: 周长: 31.42
}

输出:

面积: 78.54
周长: 31.42

解释:

  • Circle结构体定义了圆的半径。
  • AreaCircumference方法分别计算圆的面积和周长。
方法的接收者

方法的接收者可以是值类型或指针类型,不同类型的接收者具有不同的行为和性能影响。

值接收者(Value Receiver):

  • 接收者为值类型时,方法接收的是类型的副本。
  • 在方法中对接收者的修改不会影响原始变量。
  • 适用于不需要修改接收者数据的方法。

指针接收者(Pointer Receiver):

  • 接收者为指针类型时,方法接收的是类型的地址。
  • 可以在方法中修改接收者的字段,影响原始变量。
  • 避免大结构体的复制,提高性能。
  • 适用于需要修改接收者数据的方法。

示例:

package mainimport "fmt"// 定义结构体
type BankAccount struct {Owner stringBalance float64
}// 值接收者方法
func (ba BankAccount) DepositValue(amount float64) {ba.Balance += amountfmt.Printf("[DepositValue] 新余额: %.2f\n", ba.Balance)
}// 指针接收者方法
func (ba *BankAccount) DepositPointer(amount float64) {ba.Balance += amountfmt.Printf("[DepositPointer] 新余额: %.2f\n", ba.Balance)
}func main() {account := BankAccount{Owner: "John Doe", Balance: 1000}account.DepositValue(500)     // 修改的是副本fmt.Printf("余额 after DepositValue: %.2f\n", account.Balance) // 输出: 1000.00account.DepositPointer(500)   // 修改的是原始变量fmt.Printf("余额 after DepositPointer: %.2f\n", account.Balance) // 输出: 1500.00
}

输出:

[DepositValue] 新余额: 1500.00
余额 after DepositValue: 1000.00
[DepositPointer] 新余额: 1500.00
余额 after DepositPointer: 1500.00

解释:

  • DepositValue方法接收者为值类型,仅修改方法内部的副本,原始余额不变。
  • DepositPointer方法接收者为指针类型,修改的是原始余额。

选择接收者类型的建议:

  • 如果方法需要修改接收者的字段,使用指针接收者。
  • 对于大型结构体,使用指针接收者以避免复制,提高性能。
  • 如果方法不需要修改接收者,并且结构体较小,使用值接收者。
  • 为保持一致性,建议为同一类型的方法统一使用指针接收者或值接收者。
方法的嵌套与组合

虽然Go语言不支持类的继承,但可以通过结构体嵌套和组合实现类似的功能,赋予结构体更丰富的行为。

示例:

package mainimport "fmt"// 定义基础结构体
type Animal struct {Name string
}// 定义方法
func (a Animal) Speak() {fmt.Printf("%s makes a sound.\n", a.Name)
}// 定义子结构体,通过嵌套结构体实现组合
type Dog struct {AnimalBreed string
}// 重写Speak方法
func (d Dog) Speak() {fmt.Printf("%s barks.\n", d.Name)
}func main() {a := Animal{Name: "Generic Animal"}a.Speak() // 输出: Generic Animal makes a sound.d := Dog{Animal: Animal{Name: "Buddy"},Breed:  "Golden Retriever",}d.Speak() // 输出: Buddy barks.
}

输出:

Generic Animal makes a sound.
Buddy barks.

解释:

  • Dog结构体通过嵌套Animal结构体,实现了Animal的字段和方法。
  • Dog结构体重写了AnimalSpeak方法,实现了多态性。

注意事项:

  • Go语言不支持方法的覆盖(Override)和继承(Inheritance),但可以通过组合和接口实现类似的功能。
  • 通过结构体嵌套,可以实现代码复用和类型扩展,增强结构体的功能。

7.3 接口

接口(Interface)是Go语言中实现多态性的核心机制。接口定义了一组方法的签名,任何实现了这些方法的类型都满足该接口。通过接口,可以编写更加灵活和可扩展的代码。

定义接口

接口使用type关键字和interface关键字定义,包含了一组方法签名。

基本语法:

type InterfaceName interface {Method1(parameters) returnTypesMethod2(parameters) returnTypes// ...
}

示例:

package mainimport "fmt"// 定义接口
type Shape interface {Area() float64Perimeter() float64
}

解释:

  • Shape接口定义了两个方法:AreaPerimeter,均返回float64类型的值。
实现接口

在Go语言中,不需要显式声明一个类型实现了某个接口,只需定义了接口中的所有方法即可。这样的隐式实现机制提高了代码的灵活性和简洁性。

示例:

package mainimport "fmt"// 定义接口
type Shape interface {Area() float64Perimeter() float64
}// 定义结构体
type Rectangle struct {Width, Height float64
}// 实现接口方法
func (r Rectangle) Area() float64 {return r.Width * r.Height
}func (r Rectangle) Perimeter() float64 {return 2 * (r.Width + r.Height)
}// 定义另一个结构体
type Circle struct {Radius float64
}// 实现接口方法
func (c Circle) Area() float64 {return 3.14159 * c.Radius * c.Radius
}func (c Circle) Perimeter() float64 {return 2 * 3.14159 * c.Radius
}func main() {var s Shapes = Rectangle{Width: 10, Height: 5}fmt.Printf("Rectangle Area: %.2f\n", s.Area())         // 输出: Rectangle Area: 50.00fmt.Printf("Rectangle Perimeter: %.2f\n", s.Perimeter()) // 输出: Rectangle Perimeter: 30.00s = Circle{Radius: 7}fmt.Printf("Circle Area: %.2f\n", s.Area())            // 输出: Circle Area: 153.94fmt.Printf("Circle Perimeter: %.2f\n", s.Perimeter())  // 输出: Circle Perimeter: 43.98
}

输出:

Rectangle Area: 50.00
Rectangle Perimeter: 30.00
Circle Area: 153.94
Circle Perimeter: 43.98

解释:

  • RectangleCircle结构体分别实现了Shape接口的所有方法。
  • 通过接口变量s,可以统一调用不同类型的ShapeAreaPerimeter方法,实现多态性。

注意事项:

  • 一个类型实现了接口中的所有方法,就自动满足该接口,无需显式声明。
  • 接口可以嵌套其他接口,增强接口的功能。
空接口与类型断言

空接口(Empty Interface):

空接口interface{}不包含任何方法签名,所有类型都满足空接口。它通常用于需要处理任意类型的数据。

示例:

package mainimport "fmt"func printAnything(a interface{}) {fmt.Println(a)
}func main() {printAnything(100)printAnything("Hello, World!")printAnything(true)printAnything(3.14)
}

输出:

100
Hello, World!
true
3.14

解释:

  • printAnything函数接受一个空接口类型的参数,可以接收任何类型的值。

类型断言(Type Assertion):

类型断言用于将接口类型转换为具体类型。它可以检查接口值是否持有特定的类型,并安全地转换。

基本语法:

value, ok := interfaceValue.(ConcreteType)
  • value:转换后的具体类型值。
  • ok:布尔值,表示转换是否成功。

示例:

package mainimport "fmt"func main() {var i interface{} = "Go Language"s, ok := i.(string)if ok {fmt.Println("字符串长度:", len(s)) // 输出: 字符串长度: 12} else {fmt.Println("不是字符串类型")}n, ok := i.(int)if ok {fmt.Println("整数:", n)} else {fmt.Println("不是整数类型") // 输出: 不是整数类型}
}

输出:

字符串长度: 12
不是整数类型

类型切换(Type Switch):

类型切换用于根据接口值的实际类型执行不同的代码块。它是一种更简洁和安全的方式来处理多种类型。

基本语法:

switch v := interfaceValue.(type) {
case Type1:// 处理Type1
case Type2:// 处理Type2
default:// 处理其他类型
}

示例:

package mainimport "fmt"func main() {var i interface{} = 3.14switch v := i.(type) {case int:fmt.Println("整数:", v)case float64:fmt.Println("浮点数:", v)case string:fmt.Println("字符串:", v)default:fmt.Println("未知类型")}
}

输出:

浮点数: 3.14

解释:

  • type关键字用于检测接口值的实际类型,并在不同的case中执行相应的代码块。

注意事项:

  • 安全性:类型断言和类型切换可以防止运行时错误,确保类型转换的安全性。
  • 性能:频繁的类型断言可能会影响性能,应在必要时使用。

7.4 组合与继承

Go语言不支持传统的类继承,但通过结构体嵌套和组合,可以实现代码复用和类型扩展的功能。此外,通过接口,可以实现多态性,使得不同类型能够以统一的方式被处理。

结构体嵌套

结构体嵌套(Struct Embedding)是Go语言中实现组合的方式之一。通过将一个结构体嵌入到另一个结构体中,可以复用嵌入结构体的字段和方法。

示例:

package mainimport "fmt"// 定义基础结构体
type Address struct {City    stringZipCode string
}// 定义Person结构体,嵌套Address
type Person struct {Name    stringAge     intAddress // 嵌入结构体,实现组合
}// 定义方法
func (p Person) Greet() {fmt.Printf("Hello, my name is %s. I am %d years old.\n", p.Name, p.Age)
}func main() {p := Person{Name: "Eve",Age:  28,Address: Address{City:    "New York",ZipCode: "10001",},}p.Greet() // 输出: Hello, my name is Eve. I am 28 years old.fmt.Println("City:", p.City) // 直接访问嵌套结构体的字段,输出: City: New York
}

输出:

Hello, my name is Eve. I am 28 years old.
City: New York

解释:

  • Person结构体嵌套了Address结构体,Person可以直接访问Address的字段和方法。
  • 通过嵌套结构体,实现了字段和方法的复用,类似于继承。

注意事项:

  • 命名冲突:如果嵌套结构体中有与外层结构体同名的字段或方法,会导致命名冲突。Go语言会优先选择外层结构体的字段或方法。

    示例:

    package mainimport "fmt"type Animal struct {Name string
    }type Dog struct {AnimalName string // 与嵌套的Animal结构体中的Name字段冲突
    }func main() {d := Dog{Animal: Animal{Name: "Generic Animal"},Name:   "Buddy",}fmt.Println("Dog's Name:", d.Name)         // 输出: Buddyfmt.Println("Animal's Name:", d.Animal.Name) // 输出: Generic Animal
    }
    

    输出:

    Dog's Name: Buddy
    Animal's Name: Generic Animal
    
  • 方法继承:嵌套结构体的方法也会被外层结构体继承,可以直接调用。

    示例:

    package mainimport "fmt"type Animal struct{}func (a Animal) Speak() {fmt.Println("Animal speaks")
    }type Dog struct {Animal
    }func main() {d := Dog{}d.Speak() // 调用嵌套结构体的方法,输出: Animal speaks
    }
    
多态(Polymorphism)

多态性允许不同类型的对象以统一的接口进行交互。在Go语言中,通过接口实现多态性,使得不同类型的对象能够实现相同的方法集合,从而可以被同一个接口变量引用和调用。

示例:

package mainimport "fmt"// 定义接口
type Speaker interface {Speak()
}// 定义结构体1
type Human struct {Name string
}// 实现接口方法
func (h Human) Speak() {fmt.Printf("Hi, I am %s.\n", h.Name)
}// 定义结构体2
type Dog struct {Breed string
}// 实现接口方法
func (d Dog) Speak() {fmt.Printf("Woof! I am a %s.\n", d.Breed)
}func main() {var s Speakers = Human{Name: "Alice"}s.Speak() // 输出: Hi, I am Alice.s = Dog{Breed: "Golden Retriever"}s.Speak() // 输出: Woof! I am a Golden Retriever.
}

输出:

Hi, I am Alice.
Woof! I am a Golden Retriever.

解释:

  • Speaker接口定义了一个Speak方法。
  • HumanDog结构体分别实现了Speak方法。
  • 通过接口变量s,可以引用不同类型的对象,实现多态性。

注意事项:

  • 接口的实现是隐式的:只要类型实现了接口中的所有方法,就自动满足该接口,无需显式声明。
  • 接口变量的动态类型:接口变量在运行时可以持有不同类型的值,实现灵活的多态性。

7.5 指针与结构体

指针是存储变量内存地址的变量。在Go语言中,指针与结构体结合使用,可以提高程序的性能,避免大量数据的复制,同时实现对结构体的修改和共享。通过指针,可以有效地管理内存和数据,特别是在处理大型结构体或需要频繁修改数据时尤为重要。

指针基础

1. 声明指针

使用*符号声明指针类型。

var p *int

解释:

  • p是一个指向int类型的指针,初始值为nil

2. 获取变量的地址

使用&符号获取变量的内存地址。

a := 10
p := &a
fmt.Println("a的地址:", p) // 输出: a的地址: 0xc0000140b0

3. 解引用指针

使用*符号访问指针指向的值。

fmt.Println("p指向的值:", *p) // 输出: p指向的值: 10

4. 修改指针指向的值

通过指针修改变量的值。

*p = 20
fmt.Println("修改后的a:", a) // 输出: 修改后的a: 20

完整示例:

package mainimport "fmt"func main() {var a int = 10var p *int = &afmt.Println("变量a的值:", a)         // 输出: 10fmt.Println("指针p的地址:", p)       // 输出: a的地址fmt.Println("指针p指向的值:", *p)     // 输出: 10// 修改指针指向的值*p = 30fmt.Println("修改后的a:", a)         // 输出: 30
}

输出:

变量a的值: 10
指针p的地址: 0xc0000140b0
指针p指向的值: 10
修改后的a: 30
指针与结构体

将指针与结构体结合使用,可以避免复制整个结构体,尤其是当结构体较大时,提高程序的性能。此外,通过指针,可以在函数中修改结构体的字段。

1. 定义结构体并使用指针

package mainimport "fmt"// 定义结构体
type Person struct {Name stringAge  int
}func main() {p := Person{Name: "Alice", Age: 25}fmt.Println("原始结构体:", p) // 输出: {Alice 25}// 获取结构体的指针ptr := &p// 修改指针指向的结构体字段ptr.Age = 26fmt.Println("修改后的结构体:", p) // 输出: {Alice 26}
}

输出:

原始结构体: {Alice 25}
修改后的结构体: {Alice 26}

解释:

  • ptr是指向Person结构体p的指针。
  • 通过指针ptr修改Age字段,直接影响原始结构体p

2. 结构体指针作为函数参数

通过将结构体指针作为函数参数,可以在函数中修改结构体的字段,而无需返回修改后的结构体。

package mainimport "fmt"// 定义结构体
type Rectangle struct {Width, Height float64
}// 定义函数,接受结构体指针并修改字段
func Resize(r *Rectangle, width, height float64) {r.Width = widthr.Height = height
}func main() {rect := Rectangle{Width: 10, Height: 5}fmt.Println("原始矩形:", rect) // 输出: {10 5}Resize(&rect, 20, 10)fmt.Println("修改后的矩形:", rect) // 输出: {20 10}
}

输出:

原始矩形: {10 5}
修改后的矩形: {20 10}

解释:

  • Resize函数接受Rectangle结构体的指针,通过指针修改结构体的WidthHeight字段。

3. 指针接收者方法

前面章节中提到方法接收者可以是指针类型,这样可以在方法中修改结构体的字段。

package mainimport "fmt"// 定义结构体
type Counter struct {count int
}// 定义指针接收者方法
func (c *Counter) Increment() {c.count++
}func main() {c := Counter{count: 0}fmt.Println("初始计数:", c.count) // 输出: 0c.Increment()fmt.Println("计数 after Increment:", c.count) // 输出: 1// 使用指针变量cp := &ccp.Increment()fmt.Println("计数 after cp.Increment:", c.count) // 输出: 2
}

输出:

初始计数: 0
计数 after Increment: 1
计数 after cp.Increment: 2

解释:

  • Increment方法使用指针接收者,可以直接修改Counter结构体的count字段。
指针的高级用法

1. 指针与切片

切片本身是引用类型,包含指向底层数组的指针。通过指针,可以修改切片的元素,或在函数中传递切片指针以实现更灵活的操作。

示例:

package mainimport "fmt"func modifySlice(s *[]int) {(*s)[0] = 100*s = append(*s, 200)
}func main() {s := []int{1, 2, 3}fmt.Println("原始切片:", s) // 输出: [1 2 3]modifySlice(&s)fmt.Println("修改后的切片:", s) // 输出: [100 2 3 200]
}

输出:

原始切片: [1 2 3]
修改后的切片: [100 2 3 200]

解释:

  • modifySlice函数接受切片的指针,通过指针修改切片的第一个元素,并添加新的元素。

2. 指针与Map

Map是引用类型,通常不需要使用指针传递Map,因为Map本身就是引用的。但在某些情况下,可以使用指针传递Map,以便在函数中重新分配或替换整个Map。

示例:

package mainimport "fmt"func modifyMap(m *map[string]string) {(*m)["Germany"] = "Berlin"
}func main() {capitals := map[string]string{"China":  "Beijing","USA":    "Washington","Japan":  "Tokyo",}modifyMap(&capitals)fmt.Println("修改后的Map:", capitals) // 输出: map[China:Beijing Germany:Berlin Japan:Tokyo USA:Washington]
}

输出:

修改后的Map: map[China:Beijing Germany:Berlin Japan:Tokyo USA:Washington]

解释:

  • modifyMap函数接受Map的指针,通过指针添加新的键值对到Map中。

3. 指针数组

数组中可以存储指针类型的元素,适用于需要引用和共享数据的场景。

示例:

package mainimport "fmt"func main() {a, b, c := 1, 2, 3ptrArr := []*int{&a, &b, &c}for i, ptr := range ptrArr {fmt.Printf("ptrArr[%d] 指向的值: %d\n", i, *ptr)}// 修改通过指针数组修改原始变量*ptrArr[0] = 10fmt.Println("修改后的a:", a) // 输出: 10
}

输出:

ptrArr[0] 指向的值: 1
ptrArr[1] 指向的值: 2
ptrArr[2] 指向的值: 3
修改后的a: 10

解释:

  • ptrArr是一个存储指向int类型的指针数组。
  • 通过指针数组,可以修改原始变量a的值。
注意事项
  • 指针的零值:未初始化的指针为nil。在使用指针前,确保其已被正确初始化,避免运行时错误。

    示例:

    var p *int
    // fmt.Println(*p) // 运行时错误: invalid memory address or nil pointer dereference
    
  • 避免悬挂指针:确保指针指向的变量在指针使用期间保持有效,避免指针指向已经释放或超出作用域的变量。

    示例:

    func getPointer() *int {x := 10return &x
    }func main() {p := getPointer()fmt.Println(*p) // 不安全:x 已经超出作用域,可能导致未定义行为
    }
    

    解决方案: 使用堆分配(通过new函数或返回结构体实例)确保变量在函数外仍然有效。

  • 使用指针优化性能:对于大型结构体,使用指针传递可以避免复制整个结构体,提高性能。

    示例:

    type LargeStruct struct {Data [1000]int
    }func process(ls LargeStruct) { // 复制整个结构体// ...
    }func processPointer(ls *LargeStruct) { // 传递指针// ...
    }
    
  • nil指针检查:在使用指针前,最好检查指针是否为nil,以避免运行时错误。

    if p != nil {fmt.Println(*p)
    } else {fmt.Println("指针为nil")
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86673.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86673.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP语法基础篇(六):数组

PHP 中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型针对多种不同用途进行了优化;它可以被视为数组、列表(向量)、哈希表(映射的实现)、字典、集合、堆栈、队列等等。本篇文章将记录数…

GitHub Actions 的深度解析与概念介绍

GitHub Actions 核心定义 Git Actions 是 GitHub 原生提供的 自动化工作流引擎,允许开发者在代码仓库中直接创建、测试、部署代码。其本质是通过事件驱动(Event-Driven)的自动化管道,将软件开发中的重复任务抽象为可编排的流程。…

TestCafe 全解析:免费开源的 E2E 测试解决方案实战指南

在2025年的数字化浪潮中,Web应用的稳定性和用户体验成为企业竞争的关键,而端到端(E2E)测试则是确保质量的“守护者”!想象一下,您的电商平台因表单错误导致用户流失,或者支付流程因浏览器兼容性…

[CVPR 2025] 高效无监督Prompt与偏好对齐驱动的半监督医学分割

CVPR 2025 | 优化SAM:高效无监督Prompt与偏好对齐驱动的半监督医学分割 论文信息 标题:Enhancing SAM with Efficient Prompting and Preference Optimization for Semi-supervised Medical Image Segmentation作者:Aishik Konwer, Zhijian…

【C++】责任链模式

目录 一、模式核心概念与结构二、C++ 实现示例:员工请假审批系统三、责任链模式的关键特性四、应用场景五、责任链模式与其他设计模式的关系六、C++ 标准库中的责任链模式应用七、优缺点分析八、实战案例:Web 请求过滤器链九、实现注意事项如果这篇文章对你有所帮助,渴望获得…

dp进阶,树形背包(dfs+01)

顾名思义,就是在对树进行搜索的时候,由于限制了子节点选根节点必选和节点数限制,所以需要额外利用背包来维护最大值 假设根节点就是0,我们很容易 发现,这就是一个正常的树求和,但是限制了节点数量&#xf…

微信小程序安卓手机输入框文字飘出输入框

最近在开发微信小程序遇到一个问题,安卓手机输入框文字飘出输入框,但是ios系统的手机则正常。 使用情景:做了一个弹窗,弹窗内是表单,需要填写一些信息,但是在填写信息时光标不显示,输入的内容飘…

3 大语言模型预训练数据-3.2 数据处理-3.2.2 冗余去除——3.后缀数组(Suffix Array)在大模型数据去重中的原理与实战

后缀数组(Suffix Array)在大模型数据去重中的原理与实战 一、后缀数组的核心原理与数据结构二、后缀数组去重的核心流程1. **文档预处理与合并**2. **构建后缀数组**3. **计算最长公共前缀(LCP)数组**4. **基于LCP检测重复文档** …

数据库外连接详解:方式、差异与关键注意事项

🔄 数据库外连接详解:方式、差异与关键注意事项 外连接用于保留至少一个表的全部行,即使另一表无匹配记录。以下是三种外连接方式的深度解析: 🔍 一、外连接的三种类型 1. 左外连接 (LEFT OUTER JOIN) 作用&#xf…

vscode把less文件生成css文件配置,设置生成自定义文件名称和路径

1.下载less插件 在插件市场搜索 less 2.设置生成配置 3.修改out属性 "less.compile": {"compress": false, // 是否删除多余空白字符 一行显示[压缩]"sourceMap": false, // 是否创建文件目录树,true的话会自动生成一个 .css.map …

探索相机成像的奥秘 - 齐次坐标、径向失真和图像传感器倾斜

引言 大家好!今天我们将一起探索相机成像背后的一些关键技术概念:齐次坐标、径向失真和图像传感器倾斜。这些概念对于理解相机如何捕捉和处理图像至关重要。我们将通过简单易懂的语言和严谨的公式来详细解释这些概念。 齐次坐标(Homogeneou…

校企协同育人,智慧养老实训基地助力人才就业无忧

随着我国人口老龄化程度不断加深,智慧养老产业蓬勃发展,对专业人才的需求日益迫切。校企协同打造智慧养老实训基地,成为解决人才供需矛盾、提升人才培养质量的重要途径。通过科学的建设方案,智慧养老实训基地能够为学生提供实践平…

从需求到落地:一个AI训练平台的售前全流程复盘

目录 一、项目背景:客户要建自己的AI训练平台 二、需求梳理三板斧:并发量、存储带宽、模型种类 1. 并发训练量 2. 存储带宽需求 3. 模型类型与参数规模 三、解决方案设计:GPU选型 + 高速网络 + 存储架构 ✅ GPU服务器选型 ✅ 网络与通信架构 ✅ 存储与数据缓存 四…

织梦DedeCMS转WordPress

最近,有个用户找模板兔迁移网站,源站用的dede,需要转成wp,文章数量大概7000-8000篇,其中有个需求是保证旧文章的链接有效,在wp上的新文章与旧文章的链接类型不一样,所以这涉及到伪静态来处理跳转…

installGo.sh

#!/bin/bash # 检查是否以root用户运行 if [ "$(id -u)" -ne 0 ]; then echo "请使用root权限运行此脚本" exit 1 fi # 检查是否安装了必要的工具 for cmd in curl wget tar; do if ! command -v $cmd &> /dev/null; then echo…

【技术难题】el-table的全局数据排序实现示例,不受分页影响,以及异步请求带来的页面渲染问题

参考链接:https://blog.csdn.net/qq_35770559/article/details/131183121 问题代码 编辑页面detail.vue <el-form title="列表信息" name="detail"><el-form><el-form-item><el-buttontype="cyan"icon="el-icon-p…

非功能测试

非功能测试范畴&#xff1a;界面测试&#xff0c;易用性测试&#xff0c;兼容性测试&#xff0c;文档测试&#xff0c;安装/卸载测试等等 界面测试 1.窗体界面测试 1.窗体定义&#xff1a;指整个软件窗口&#xff0c;也可称为窗口&#xff0c;是界面测试的基本单位 2.控件分…

一起endpoint迷路的问题排查总结

今天上班&#xff0c;一到工位上&#xff0c;就有同事和我说有客户反映自己的容器的一些指标在监控平台不上报了&#xff0c;我当时一看机器所在的监控&#xff0c;发现确实是这样 确实存在某个点开始数据就没了&#xff0c;主要这个点当时也没有任何的操作变更&#xff0c;于…

官方 Linker Scripts 语法和规则解析(2)

系列文章目录 官方 Linker Scripts 语法和规则解析&#xff08;1&#xff09; 官方 Linker Scripts 语法和规则解析&#xff08;2&#xff09; 官方 Linker Scripts 语法和规则解析&#xff08;3&#xff09; 链接脚本(Linker Scripts)语法和规则解析(自官方手册) 7.9. 链接脚…

CentOS 7 通过YUM安装MySQL 8.0完整指南

一、准备工作&#xff1a;更新系统与YUM源 # 1. 更换阿里云镜像源 wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo# 2. 清理并重建缓存 yum clean all yum makecache# 3. 升级系统所有包 yum -y update 二、安装MySQL 8.0 1. 下载…