1、结构体

结构体

Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性,结构体是类型中带有成员的复合类型,属于值类型

Go 语言中的类型可以被实例化,使用new()&可以实例化该类型、获取实例的指针。

结构体成员是由一系列的成员变量构成,这些成员变量也被称为字段。字段有以下特性:

同样对于可见性,结构体的名称、结构体字段、结构体方法的名称首字母大小写规则和方法、变量名称规则相同。

定义

使用关键字type可以将各种基本类型定义为自定义类型,基本类型包括int、string、bool等。

结构体是一种复合的基本类型,通过type定义为自定义类型后,使结构体更便于使用。

type structName struct {
	filed1Name filed1Type
	filed2Name filed2Type
	filed3Name filed3Type
}

实例化

type Stu struct {
	name string
	age  int
}

使用var

由于结构体属于基本数据类型(值类型),所以在声明时,系统会自动对结构体以及结构体字段赋零值

使用var关键字

var s Stu
fmt.Println(s) // main.Stu{name:"", age:0}

使用new

sp := new(Stu)
s := *sp
fmt.Printf("%#v\n", s) // main.Stu{name:"", age:0}

初始化并赋初始值

可以在大括号中以键值对成员声明顺序的方式进行赋值

s1 := Stu{}
fmt.Printf("%#v\n", s1) // main.Stu{name:"", age:0}

s2 := Stu{"lucy", 18}
fmt.Printf("%#v\n", s2) // main.Stu{name:"lucy", age:18}

s3 := Stu{name: "tom", age: 25}
fmt.Printf("%#v\n", s3) // main.Stu{name:"tom", age:25}

// 这种方法也可以直接获取结构体指针
s := &Stu{"lucy", 18}
fmt.Printf("%T\n", s)  // *main.Stu
fmt.Printf("%#v\n", s) // &main.Stu{name:"lucy", age:18}

读写字段

结构体变量结构体指针都可以使用操作符.对进行读写操作

package main

import "fmt"

type Stu struct {
	name string
	age  int
}

func main() {
	s := Stu{"lucy", 18}
	s.name = "tom"
	fmt.Println(s) // {tom 18}

	sp := &s
	sp.name = "lily"
	fmt.Println(s) // {lily 18}
}

结构体方法与接收者

在Go语言中,没有类的概念但是可以给结构体、自定义类型定义方法。Go 方法是作用在接收者(receiver)上的一个函数,接受者就是结构体、自定义类型的实例,类似于其他语言中的thisself

使用接收者,就可以直接在实例后使用.来调用方法,并且操作实例

非本地类型不可定义方法,也就是不可以给别的包的类型定义方法。

package main

import (
	"fmt"
)

type Stu struct {
	name string
	age  int
}

// 定义结构体Stu的方法
func (s *Stu) ShowInfo() {
	fmt.Printf("the user name is %v, age is %d\n", s.name, s.age)
}

func main() {
	s := Stu{"lucy", 18}
	s.ShowInfo() // the user name is lucy, age is 18
}

img

toString

在Java里面,实例的信息字符串一般由toString方法提供,在go里面,是由String() string函数提供,其原理也是实现了接口的方法

package main

import (
	"fmt"
)

type Stu struct {
	name string
	age  int
}

func (s Stu) String() string {
	return fmt.Sprintf("the user name is %v, age is %d\n", s.name, s.age)
}

func main() {
	s := Stu{"lucy", 18}
	fmt.Println(s)  // the user name is lucy, age is 18
	fmt.Println(&s) // the user name is lucy, age is 18
}

内嵌类型和结构体

内嵌类型

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字

package main

import "fmt"

type Stu struct {
	int
	string
}

func main() {
	s := Stu{18, "lucy"}
	fmt.Println(s.int, s.string) // 18 lucy
}

内嵌结构体(重要)

Go语言中的继承是通过内嵌或组合来实现的

同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用。内层结构体被简单的插入或者内嵌进外层结构体,外层结构体实例可以直接访问内嵌结构体成员。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。

内嵌结构体可以对外层的结构体进行功能增强,外层结构体可以直接调用内层的方法

package main

import "fmt"

type Father struct {
	name string
	age  int
}
type Chil struct {
	id int
	Father
}

func main() {
	c := Chil{111, Father{"lucy", 18}}
	fmt.Printf("id:%d,name:%s,age:%d", c.id, c.name, c.age)
}

Go语言的结构体内嵌有如下特性:

Go 中的面向对象编程

Go与传统面向对象语言的区别

特性 传统面向对象语言 Go语言
有类的概念,作为对象的蓝图 没有类,使用结构体定义数据结构
构造函数 专门的构造方法 使用普通函数创建和初始化结构体
继承 通过类继承实现代码复用和多态 没有继承,使用组合和接口
多态 通过继承和方法重写实现 通过接口实现
封装 通过访问修饰符控制可见性 通过大小写控制可见性
方法重载 支持同名不同参数的方法 不支持方法重载
异常处理 通常使用try-catch机制 使用多返回值和错误处理
范型(Go 1.18前) 大多支持 不支持,使用接口和反射

构造函数

Go没有专门的构造函数,但可以创建返回初始化结构体的函数,这是一种常见的惯例:

构造函数的优势:

  1. 提供参数验证和默认值
  2. 确保结构体正确初始化
  3. 封装复杂的初始化逻辑
  4. 可以返回接口而非具体类型,提高灵活性
// NewPerson作为Person的构造函数
func NewPerson(firstName, lastName string, age int, address string) *Person {
    // 可以在这里进行参数验证
    if age < 0 {
        age = 0
    }
    
    return &Person{
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
        Address:   address,
    }
}

func main() {
    // 使用构造函数创建实例
    p := NewPerson("张", "三", 30, "北京市海淀区")
    fmt.Printf("%+v\n", *p)
}

Go OOP最佳实践

在Go中实现面向对象编程时,应该遵循一些最佳实践,确保代码的可读性、可维护性和性能。

设计原则

优先使用组合:优先使用组合而非模拟继承。

// 不推荐:模拟继承
type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "某种声音"
}

type Dog struct {
    Animal  // 试图模拟继承
    Breed string
}

// 推荐:明确组合
type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "某种声音"
}

type Dog struct {
    Animal Animal  // 清晰表明这是组合
    Breed  string
}

func (d Dog) Speak() string {
    return "汪汪"
}

接口应该小而精确:定义最小可行的接口。

// 不推荐:过大的接口
type FileProcessor interface {
    Open(filename string) error
    Read() ([]byte, error)
    Process() error
    Write(data []byte) error
    Close() error
}

// 推荐:小而专注的接口
type Reader interface {
    Read() ([]byte, error)
}

type Writer interface {
    Write(data []byte) error
}

type Processor interface {
    Process() error
}

// 可以组合使用
type FileHandler struct {
    // ...
}

func (f *FileHandler) Read() ([]byte, error) {
    // 实现读取
}

func (f *FileHandler) Write(data []byte) error {
    // 实现写入
}

func (f *FileHandler) Process() error {
    // 实现处理
}

避免接口污染:不要在结构体上定义不需要的方法仅为了满足接口。

// 不推荐:为了满足接口添加不必要的方法
type Logger interface {
    Log(message string)
    LogError(err error)
    LogWarning(message string)
}

type SimpleLogger struct{}

func (l SimpleLogger) Log(message string) {
    fmt.Println(message)
}

// 这些方法仅为了满足接口
func (l SimpleLogger) LogError(err error) {
    l.Log(err.Error()) // 只是转发到Log
}

func (l SimpleLogger) LogWarning(message string) {
    l.Log("WARNING: " + message) // 只是转发到Log
}

// 推荐:使用适配器模式
type Logger interface {
    Log(message string)
    LogError(err error)
    LogWarning(message string)
}

type SimpleLogger struct{}

func (l SimpleLogger) Log(message string) {
    fmt.Println(message)
}

// 使用适配器满足复杂接口
type LoggerAdapter struct {
    logger SimpleLogger
}

func (a LoggerAdapter) Log(message string) {
    a.logger.Log(message)
}

func (a LoggerAdapter) LogError(err error) {
    a.logger.Log(err.Error())
}

func (a LoggerAdapter) LogWarning(message string) {
    a.logger.Log("WARNING: " + message)
}

适应Go的思维方式

要在Go中成功应用面向对象编程,需要调整思维方式:

  1. 放弃继承思维,转向组合思维。
  2. 设计小而精确的接口,而非大型类层次结构。
  3. 关注类型的行为(方法),而非它继承自什么。
  4. 优先考虑数据和行为的分离,而非强制捆绑。
  5. 使用显式代码而非魔法和隐式行为。