【Goland】:面向对象编程
目录
1. 结构体
1.1 创建结构体
1.2 结构体内存对齐
1.3 结构体类型转换
1.4 字段的Tag标签
2. 方法
2.1 值接收者 vs 指针接收者
2.2 String方法
3. 封装
3.1 封装的概念
3.2 工厂函数
4. 继承
4.1 go的继承
4.2 结构体字段/方法的查找规则
5. 接口
5.1 概念
5.2 接口继承
5.3 空接口
5.4 自定义类型序列排序
6. 类型断言
6.1 概念
6.2 类型断言 + switch
1. 结构体
在 Go 语言中,struct
是一种用户自定义的数据类型,用来 封装一组不同类型的数据字段。
可以类比成 “对象的属性集合”,但 Go 没有类(class),只有结构体和方法。
type Person struct {Name stringAge intIsVIP bool
}
1.1 创建结构体
package mainimport "fmt"type Person struct {Name stringAge intIsVIP bool
}func main() {// 方式一:直接赋值p1 := Person{"张三", 18, true}// 方式二:指定字段(推荐,可读性高)p2 := Person{Name: "李四", Age: 20}// 方式三:new 创建(返回指针)p3 := new(Person)p3.Name = "王五"p3.Age = 22// 方式四:取地址 &p4 := &Person{Name: "赵六", Age: 25}
}
1.2 结构体内存对齐
可以看看这一篇博客
结构体内存对齐原理与规则解析-CSDN博客
1.3 结构体类型转换
1. 同类型之间的赋值
两个结构体的 字段名、字段类型、顺序完全相同,即便它们是不同的类型定义,也可以 强制转换。
package mainimport "fmt"type Person struct {Name stringAge int
}type User struct {Name stringAge int
}func main() {p := Person{"Tom", 18}// 类型不同,但字段一致,可以强转u := User(p)fmt.Println(u) // {Tom 18}
}
2. 不同结构体之间的转换(字段不完全一致)
如果字段不一致,Go 不允许强制转换,必须手动赋值。
package mainimport "fmt"type Person struct {Name stringAge int
}type User struct {Name stringAge intid int
}func main() {p := Person{"Tom", 18}// 类型不同,但字段一致,可以强转u := User{Name: p.Name,Age: p.Age,id: 1,}fmt.Println(u) // {Tom 18 1}
}
1.4 字段的Tag标签
- 结构体字段后面可以跟一段字符串,用来作为元信息,这就是 Tag。
- Tag 常用来给 反射、序列化/反序列化 库提供额外信息。
// json:"name" 指定 JSON 序列化时用 "name" 作为 key
// db:"username" 指定数据库字段名为 "username"
// validate:"required" 用于校验库,表示该字段必填
type User struct {Name string `json:"name" db:"username" validate:"required"`Age int `json:"age" db:"age" validate:"gte=0,lte=120"`
}
Tag 的语法规则
-
Tag 必须是 字符串字面量,用 反引号 `` 包裹。
-
一个字段可以有多个 tag,不同库之间用 空格分隔。
-
每个 tag 是
key:"value"
形式。
FieldName Type `key1:"value1" key2:"value2"`
2. 方法
方法(Method)就是带有接收者(receiver)的函数。也就是说,方法和函数本质上是一样的,只是方法会绑定到某个类型上。
对比点 | 函数 | 方法 |
---|---|---|
定义形式 | func 函数名(...) | func (接收者) 方法名(...) |
是否绑定类型 | 否 | 是(绑定到某个类型) |
调用方式 | 函数名(...) | 实例.方法名(...) |
func (接收者 参数名) 方法名(参数列表) 返回值列表 {// 方法体
}
-
接收者:可以是某个类型的值或者指针
-
方法名:自定义,通常首字母大写可导出
-
参数列表:和普通函数一样
-
返回值列表:和普通函数一样
package mainimport "fmt"type Person struct {Name stringAge int
}func (p Person) Print() {fmt.Println("Name:", p.Name, " Age:", p.Age)
}func main() {p1 := Person{"张三", 18}p1.Print() // Name: 张三 Age: 18
}
2.1 值接收者 vs 指针接收者
-
值接收者:方法操作的是接收者的副本,不会影响原值。
-
指针接收者:方法操作的是接收者的地址,可以修改原值。
func (p Person) SetName(name string) {p.Name = name
}func (p *Person) SetRealName(name string) {p.Name = name
}
2.2 String方法
只要一个类型实现了 String() string
方法,那么:
-
使用
fmt.Print
、fmt.Println
、fmt.Sprintf("%s", obj)
打印该对象时,都会自动调用它的String()
方法。
package mainimport "fmt"type Person struct {Name stringAge int
}func (p Person) String() string {return fmt.Sprintln("名字:", p.Name, " 年龄:", p.Age)
}func main() {p1 := Person{"张三", 18}fmt.Println(p1) // 名字: 张三 年龄: 18
}
总结:
-
String()
必须返回string
类型 -
推荐用
fmt.Sprintf
来拼接字符串,而不是手动拼接 -
避免
String()
方法里又调用fmt.Sprint(p)
,否则会无限递归
3. 封装
3.1 封装的概念
-
把数据和方法绑定在一起,对外只暴露必要的接口(函数/方法),隐藏内部实现细节。
-
外部代码不能随意修改对象的内部状态,必须通过接口进行操作。
Go 中的封装特点,和 C++ 不同,Go 没有 public / private / protected
关键字,而是通过 命名约定 实现封装:
-
大写开头 → 公共(Public / 可导出),可以被其他包访问
-
小写开头 → 私有(Private / 不可导出),只能在当前包内使用
package persontype Person struct {name string // 私有字段,外部不可直接访问Age int // 公共字段,外部可直接访问
}// 构造函数(工厂函数)
func NewPerson(name string, age int) *Person {return &Person{name: name, Age: age}
}// 公共方法:获取 name
func (p *Person) GetName() string {return p.name
}// 公共方法:设置 name
func (p *Person) SetName(newName string) {p.name = newName
}
3.2 工厂函数
如果结构体字段首字母小写(私有),那么外部包是不能直接访问和初始化的,为了让外部能安全创建对象,我们可以使用一个 工厂函数 来返回结构体实例。
-
工厂函数就是一个普通函数,用来创建并返回结构体实例;
-
常见命名:
NewXxx
-
主要作用是:隐藏结构体实现、提供初始化逻辑、保证数据安全。
package modeltype person struct {name stringage int
}// 工厂函数
func NewPerson(name string, age int) *person {return &person{name: name,age: age,}
}
4. 继承
4.1 go的继承
其实严格来说,Go 没有传统意义上的类继承,它主要通过 结构体嵌套(匿名字段)+ 接口 来实现 代码复用 和 多态。
-
通过 结构体嵌套(匿名字段),一个结构体可以“继承”另一个结构体的字段和方法。
-
外层结构体可以直接访问内层结构体的方法和字段(如果名字没有冲突)。
-
这种机制更准确叫 组合(Composition),但效果类似继承。
实现效果如下:
package mainimport "fmt"type Person struct {Name stringAge int
}func (p *Person) GetName() {fmt.Println("名字是:", p.Name)
}type Student struct {PersonGrade int
}func (stu *Student) GetGrade() {fmt.Println(stu.Name, "的成绩是:", stu.Grade)
}// 工厂模式
func NewStudent(name string, age int, grade int) *Student {return &Student{Person: Person{Name: name,Age: age,},Grade: grade,}
}
func main() {stu := NewStudent("李四", 15, 100)stu.GetName() // 名字是: 李四stu.GetGrade() // 李四 的成绩是: 100
}
4.2 结构体字段/方法的查找规则
-
先在当前结构体中查找,如果找到则使用当前的。
-
如果没找到,就去看它嵌入的 匿名字段(匿名结构体) 中有没有同名的字段或方法。
-
如果匿名字段本身也嵌套了别的匿名字段,就继续向下递归查找。如果一直找不到,就会报错
示例:字段和方法访问流程
-
优先级:当前结构体 > 直接嵌入的匿名结构体 > 更深层匿名结构体。
-
如果出现方法重写(比如 animal 和 cat 都有 GetName),调用时会优先使用“外层的”实现。
-
想访问被覆盖的内层方法,需要显式指定:
a.animal.GetName()
package mainimport "fmt"type animal struct {name stringid int
}func (a animal) GetName() {fmt.Println("run animal GetName()")
}type cat struct {animalname string
}func (c cat) GetName() {fmt.Println("run cat GetName()")
}
func main() {a := cat{animal: animal{name: "cat",id: 1,},name: "dog",}// 1. 先查当前结构体 cat,在 cat 中找到 name,直接使用fmt.Println("a.name:", a.name)// 2. 当前没有 id,继续找嵌套的 animal,在 animal 中找到 idfmt.Println("a.id:", a.id)// 方法调用a.GetName() // run cat GetName()a.animal.GetName() // run animal GetName()
}
5. 接口
5.1 概念
-
接口是一种抽象类型,用于定义一组方法的签名,但不包含实现。
-
隐式实现:类型不需要显式声明实现接口,只要提供了接口中所有方法,即认为实现了该接口。
-
抽象与多态:接口允许对不同类型实现统一的操作,实现面向接口的编程。
- 一个自定义类型要实现某个接口,就必须将接口中定义的所有方法都实现。
- 接口本身不能创建实例,但可以指向一个实现了该接口的自定义类型的变量。
type 接口名 interface {方法名1(参数列表) 返回值列表方法名2(参数列表) 返回值列表
}
示例:接口的实现如下
package mainimport "fmt"// 定义接口
type Device interface {TurnOn()TurnOff()
}// 打印机实现 Device 接口
type Printer struct{}func (p Printer) TurnOn() {fmt.Println("Printer is now ON...")
}func (p Printer) TurnOff() {fmt.Println("Printer is now OFF...")
}// 扫描仪实现 Device 接口
type Scanner struct{}func (s Scanner) TurnOn() {fmt.Println("Scanner is now ON...")
}func (s Scanner) TurnOff() {fmt.Println("Scanner is now OFF...")
}// 电脑操作设备
type Computer struct{}func (c Computer) Use(d Device) { // 参数是 Device 接口类型d.TurnOn()d.TurnOff()
}func main() {computer := Computer{}printer := Printer{}scanner := Scanner{}computer.Use(printer)computer.Use(scanner)
}
5.2 接口继承
- 在定义接口时,一个接口可以继承多个其他接口
- 此时如果一个自定义类型要实现该接口,除了需要实现该接口中定义的方法外
- 还需要实现该接口继承的其他接口中定义的方法
type 接口A interface {方法1()方法2()
}type 接口B interface {接口A // 继承 接口A 的所有方法方法3()
}
示例
package mainimport "fmt"// 定义一个基础接口
type Animal interface {Eat()Sleep()
}// 定义一个扩展接口,继承了 Animal
type Dog interface {AnimalBark()
}// 定义一个结构体
type Husky struct {Name string
}// 实现 Animal 的方法
func (h Husky) Eat() {fmt.Println(h.Name, "正在吃饭")
}func (h Husky) Sleep() {fmt.Println(h.Name, "正在睡觉")
}// 实现 Dog 的方法
func (h Husky) Bark() {fmt.Println(h.Name, "汪汪叫")
}func main() {var d Dog = Husky{Name: "哈士奇"}d.Eat() // 哈士奇 正在吃饭d.Sleep() // 哈士奇 正在睡觉d.Bark() // 哈士奇 汪汪叫
}
5.3 空接口
在 Go 中,空接口 是没有任何方法定义的接口:
interface{}
// 因为它没有方法,所以 所有类型都实现了空接口。
// 这就意味着:一个 interface{} 类型的变量,可以存储任意类型的值。
1. 作为函数参数(万能参数)
package mainimport "fmt"// PrintAny 可以接收任意类型的参数。
func PrintAny(val interface{}) {fmt.Println("值是:", val)
}func main() {PrintAny("hello")PrintAny(100)PrintAny(3.1415)PrintAny([]int{1, 2, 3})PrintAny(map[string]int{"a": 1,"b": 2,})
}
2. 配合类型断言
有时候我们存放到空接口里的值,需要取出来,还原成原来的类型。可以用 类型断言:
package mainimport "fmt"func main() {var i interface{} = "golang"// 类型断言str, ok := i.(string)if ok {fmt.Println("字符串:", str)}num, ok := i.(int)if !ok {fmt.Println("不是 int 类型")}fmt.Printf("类型:%T\n", num)
}
3. 空接口的切片(类似万能数组)
package mainimport "fmt"func main() {arr := []interface{}{123, "hello", true, 3.14}for _, v := range arr {fmt.Println(v)}
}
5.4 自定义类型序列排序
Go标准库的sort包中提供了Sort函数,可用于对自定义类型序列进行排序。该函数的签名如下:
func Sort(data Interface)
Sort函数接收一个Interface类型的参数,并对其进行原地排序。Interface在sort包中是一个接口类型,接口中定义了三个方法,因此要使用Sort函数进行排序,需要先让自定义类型序列实现Interface接口。Interface接口的定义如下:
-
Len() int:返回序列长度。
-
Less(i, j int) bool:定义 排序规则,如果返回
true
,表示第i
个元素应该排在第j
个元素前面。 -
Swap(i, j int):交换切片中下标
i
和j
的元素。
type Interface interface {Len() intLess(i, j int) boolSwap(i, j int)
}
-
Len
:告诉排序函数“有多少元素”。 -
Less
:告诉排序函数“怎么比较两个元素”。 -
Swap
:告诉排序函数“如何交换元素”。
示例如下:
package mainimport ("fmt""sort"
)type Person struct {Name stringAge int
}
type Persons []Personfunc (p Persons) Len() int {return len(p)
}
func (p Persons) Less(i, j int) bool {return p[i].Age < p[j].Age
}
func (p Persons) Swap(i, j int) {p[i], p[j] = p[j], p[i]
}func main() {p := Persons{{"张三", 89},{"李四", 67},{"王五", 75},{"赵六", 99},}sort.Sort(p) // 排序fmt.Println(p)
}
6. 类型断言
6.1 概念
在 Go 里,接口变量可以存放任意实现了接口的值。但是当我们要取出 具体的底层类型 时,需要用 类型断言(type assertion)。
value, ok := x.(T)
// x:接口类型变量
// T:要断言的目标类型
// value:如果断言成功,得到的就是接口中存放的具体值(类型为 T),如果断言失败,就是 T 的零值
// ok:布尔值,表示断言是否成功(避免运行时 panic)
基本示例
package mainimport "fmt"func main() {var i interface{} = "hello"// 类型断言str, ok := i.(string)if ok {fmt.Println("断言成功:", str) // 输出: hello}num, ok := i.(int)if !ok {fmt.Println("断言失败,i 不是 int 类型")}fmt.Printf("类型:%T, 值:%d\n", num, num)
}结果:
断言成功: hello
断言失败,i 不是 int 类型
类型:int, 值:0
在接口使用的示例
package mainimport "fmt"// 定义接口
type Device interface {TurnOn()TurnOff()
}// 打印机实现 Device 接口
type Printer struct{}func (p Printer) TurnOn() {fmt.Println("Printer is now ON...")
}func (p Printer) TurnOff() {fmt.Println("Printer is now OFF...")
}// 扫描仪实现 Device 接口
type Scanner struct{}func (s Scanner) TurnOn() {fmt.Println("Scanner is now ON...")
}func (s Scanner) TurnOff() {fmt.Println("Scanner is now OFF...")
}
func (s Scanner) Hello() {fmt.Println("Scanner is now HELLO...")
}// 电脑操作设备
type Computer struct{}func (c Computer) Use(d Device) { // 参数是 Device 接口类型d.TurnOn()if Scanner, ok := d.(Scanner); ok {Scanner.Hello()}d.TurnOff()
}func main() {computer := Computer{}printer := Printer{}scanner := Scanner{}computer.Use(printer)computer.Use(scanner)
}
结果:
Printer is now ON...
Printer is now OFF...
Scanner is now ON...
Scanner is now HELLO...
Scanner is now OFF...
6.2 类型断言 + switch
基本语法:
-
x
必须是接口类型(如interface{}
)。 -
(type)
关键字只能用在switch
中。 -
每个
case
是一种具体类型。 -
v
会被自动转换成对应的类型,供case
内部使用。
switch v := x.(type) {
case T1:// x 是 T1 类型
case T2:// x 是 T2 类型
default:// 未匹配任何类型
}
示例如下:
package mainimport "fmt"type Student struct {Name stringAge intGender string
}func TypeJudge(values ...interface{}) {for i := 0; i < len(values); i++ {switch values[i].(type) {case int:fmt.Printf("第%d个参数: value = %v, type = int\n", i+1, values[i])case float64:fmt.Printf("第%d个参数: value = %v, type = float64\n", i+1, values[i])case bool:fmt.Printf("第%d个参数: value = %v, type = bool\n", i+1, values[i])case Student:fmt.Printf("第%d个参数: value = %v, type = Student\n", i+1, values[i])case *Student:fmt.Printf("第%d个参数: value = %v, type = *Student\n", i+1, values[i])default:fmt.Printf("第%d个参数: value = %v, type unknown\n", i+1, values[i])}}
}func main() {stu := Student{}TypeJudge(10, 3.1415, true, stu, &stu, "Hello World", 1)
}
结果:
第1个参数: value = 10, type = int
第2个参数: value = 3.1415, type = float64
第3个参数: value = true, type = bool
第4个参数: value = { 0 }, type = Student
第5个参数: value = &{ 0 }, type = *Student
第6个参数: value = Hello World, type unknown
第7个参数: value = 1, type = int