【Go】C++ 转 Go 第(四)天:结构体、接口、反射、标签 | 面向对象编程

本专栏文章持续更新,新增内容使用蓝色表示。

食用指南

本文适合 C++ 基础 的朋友,想要快速上手 Go 语言。可直接复制代码在 IDE 中查看,代码中包含详细注释和注意事项。

Go 的环境搭建请参考以下文章:

【Go】C++ 转 Go 第(一)天:环境搭建 Windows + VSCode 远程连接 Linux -CSDN博客

1. 结构体(Struct)

Go 中的结构体类似于 C++ 的类,是组织数据的基础单元。

Go 的结构体本质上是数据的集合,不包含继承关系,没有虚函数表,没有构造函数和析构函数的概念。

结构体默认采用值传递,与 C++ 需要开发者显式选择值语义或引用语义不同。在 Go 中,当你将一个结构体赋值给另一个变量时,发生的是完整的内存拷贝。减少了悬空指针和内存错误的风险。

Go 有垃圾回收机制,开发者不需要手动管理结构体的内存生命周期。同时,通过指针接收者,开发者可以明确控制哪些方法会修改原对象,哪些不会。

Go 复制代码
package main

import "fmt"

// 声明一种新类型
// 类似C++ 中的 typedf,但 go 对类型要求更严格
// type myint int

// 定义一个结构体
type Book struct {
	name string
	auth string
}

func main() {
	var book1 Book
	book1.auth = "王雪迎"
	book1.name = "MySQL高可用实践"
	fmt.Println(book1)
}

2. 面向对象

2.1 类与封装

Go 的封装机制与 C++ 的面向对象封装有根本不同,它采用了一种更加简单和统一的访问控制模型:

通过标识符首字母大小写控制访问权限,大写字母开头的标识符对外包可见,小写字母开头的标识符仅限包内使用。避免了 C++ 中复杂的 public、protected、private 访问说明符。

其次 Go 没有 C++ 的 friend 关键字,访问控制严格基于包边界。这意味着你无法让外部包访问某个包的私有成员。

Go 复制代码
// 封装
package main

import "fmt"

// go 语言中的类通过结构体来绑定方法
// Go 语言的访问控制是基于包(package)的,不是基于类的。
// 所以在本包范围内大小写开头都可以访问到
// 类名首字母大写,表示对外开发
type Book struct {
	// 属性名同理
	Name  string
	Auth  string
	level int
}

func (this Book) GetName() {
	fmt.Printf("Book name is %s", this.Name)
}
func (this Book) SetLevel(level int) {
	// this是调用该方法的对象的一个副本
	// 读不受影响,但是想要修改需要传指针
	this.level = level
}

// 解决方法:使用指针
func (this *Book) SetTrueLevel(level int) {
	// this是调用该方法的对象的一个副本
	this.level = level
}

func main() {
	// 声明一个对象
	fmt.Println("========初始情况=======")
	book1 := Book{
		Auth: "王雪迎",
		Name: "MySQL高可用实践", // 未初始化的level默认是0
	}
	fmt.Println(book1)

	fmt.Println("\n========调用 SetLevel 方法=======")
	book1.SetLevel(3)
	fmt.Println(book1)

	fmt.Println("\n========调用 SetTrueLevel 方法=======")
	book1.SetTrueLevel(3)
	fmt.Println(book1)
}

2.2 继承

Go 语言中并没有传统的继承机制(如 Java 的 extends 或 C++ 的基类继承),而是通过结构体嵌套的组合方式实现类似功能。

子结构体可以定义与父结构体相同的方法,从而覆盖嵌套结构体的方法,实现类似方法重写的功能。需要调用被覆盖的方法时,可以显式调用嵌套结构体的方法。

Go 复制代码
// 继承
// go的继承感觉更像嵌套组合了一下
package main

import "fmt"

type Creature struct {
	Typename string
	name     string
}

func (this *Creature) Eat() {
	if this.name != "" {
		fmt.Println(this.name, "is eating.")
	} else {
		fmt.Println("this creature is eating.")
	}
}

func (this *Creature) sleep() {
	if this.name != "" {
		fmt.Println(this.name, "is sleeping.")
	} else {
		fmt.Println("this creature is sleeping.")
	}
}

// go 没有像 C++ 的继承去分private、public、protected
type Human struct {
	Creature
	skin string
}

// 重定义父类方法
func (this *Human) sleep() {
	if this.name != "" {
		fmt.Println(this.name, "is sleeping on the bed.")
	} else {
		fmt.Println("this human is sleeping.")
	}
}

// 增加子类新方法
func (this *Human) Speak() {
	if this.name != "" {
		fmt.Println("I am", this.name, ",my skin is", this.skin)
	} else {
		fmt.Println("my skin is", this.skin)
	}
}
func main() {
	dobby := Creature{
		Typename: "dog",
		name:     "dobby",
	}
	dobby.sleep()

	Jade := Human{Creature{"human", "Jade"}, "yellow"}
	Jade.Speak()
	Jade.Eat()

	fmt.Println("\n==重写后调用sleep方法:")
	Jade.sleep()
	fmt.Println("\n==重写后调用原sleep方法:")
	Jade.Creature.sleep() // C++中使用作用域解析符
}

2.3 多态与接口

Go 通过接口实现多态,这是与 C++ 虚函数机制不同的地方。

Go 复制代码
// 多态
// 同一个接口(或父类引用),由于所指向的具体对象不同,而表现出不同的行为。
// C++ 通过虚函数机制实现多态,Go 通过预留接口实现
package main

import "fmt"

// interface本质上是一个指针
type AnimalIF interface {
	Sleep()
	GetColor() string
	GetType() string
}

// 具体的类1
type Cat struct {
	color string
}

// 实现完全接口的内容
func (this *Cat) Sleep() {
	fmt.Println("Cat is sleeping.")
}

func (this *Cat) GetColor() string {
	return this.color
}

func (this *Cat) GetType() string {
	return "Cat"
}

// 具体的类2
type Dog struct {
	color string
}

func (this *Dog) Sleep() {
	fmt.Println("Dog is sleeping.")
}

func (this *Dog) GetColor() string {
	return this.color
}

func (this *Dog) GetType() string {
	return "Dog"
}

// 不确定 animal 的具体类型
func ShowAnimal(animal AnimalIF) {
	fmt.Println("\n=====", animal.GetType())
	animal.Sleep()
	fmt.Println(animal.GetColor())
}
func main() {
	// 父类指针
	var animal AnimalIF
	animal = &Cat{"white"}
	fmt.Println("=====", animal.GetType())
	animal.Sleep()
	fmt.Println(animal.GetColor())

	animal = &Dog{"black"}
	ShowAnimal(animal)

	yellowCat := Cat{"yellow"}
	ShowAnimal(&yellowCat)
}

3. 接口

3.1 空接口与类型断言

空接口 interface{} 可以表示任何类型,配合类型断言使用。

Go 复制代码
package main

import "fmt"

// interface是万能类型
func myFunc(arg interface{}) {
	fmt.Println(arg)
	// interface 通过类型断言机制区分底层的数据类型
	// 可以根据不同的数据类型,写不同的触发场景和业务
	value, ok := arg.(string)
	if ok {
		fmt.Println("↑ is string, value is", value)
	}
	// 如果变量名还是ok,使用=即可,使用:=相当于声明并赋值
	_, ok = arg.(int)
	if ok {
		fmt.Println("↑ is int")
	}
}

type Phone struct {
	name  string
	color string
}

func main() {
	myFunc("hello world")
	myFunc(19)
	myPhone := Phone{"xiaomi", "blue"}
	myFunc(myPhone)
}

3.2 接口的底层实现

底层的内容没有发生变化。

Go 复制代码
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// 变量内部为(type, value) 对,会在赋值和类型断言时被传递和转换。
	// type 分为static type(静态类型)和concrete type(具体类型/运行时类型)

	// Ctrl + 左键 点击函数名跳转定义
	// tty : (type: *os File, value: "/dev/tty" 的文件描述符)
	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
	if err != nil {
		fmt.Println("open file error", err)
	}

	var r io.Reader
	// r : (type: *os File, value: "/dev/tty" 的文件描述符)
	// 通过 r 只能调用 Read 方法,看不到 *os.File 的其他方法(如 Write)
	r = tty

	var w io.Writer
	// w : (type: *os File, value: "/dev/tty" 的文件描述符)
	w = r.(io.Writer)

	w.Write([]byte("Hello world!\n"))
}

3.3 接口转换

Go 不支持函数/方法的重载,即不能在同一个作用域中定义多个函数名相同但参数不同的函数

但可以使用接口+类型断言模拟。(方法之一)

Go 复制代码
package main

import "fmt"

// 定义接口
type Reader interface {
	ReadBook()
}

type Writer interface {
	WriteBook()
}

// 实现了两个接口
type Book struct {
}

func (this *Book) ReadBook() {
	fmt.Println("Book is Reading.")
}

func (this *Book) WriteBook() {
	fmt.Println("Book is Writing.")
}

func main() {
	mybook := &Book{}
	// 方法接收者是 *Book 类型,不是 book 类型
	// 此处也可以采用以下方式
	// mybook := Book{}
	// r=&mybook

	var r Reader
	r = mybook
	r.ReadBook()

	var w Writer
	// 变量 r 底层指向的具体类型是 *Book,而 *Book 类型同时实现了 Reader 和 Wirter 接口。
	w = r.(Writer)
	w.WriteBook()
}

4. 反射机制

反射是Go语言的强大特性,可以在运行时动态获取类型信息。

Go 复制代码
// 反射可以帮助我们在写日常业务的过程中,动态获取未知变量的 type 和 value
// 详细部分看文档
package main

import (
	"fmt"
	"reflect"
)

func ReflectNum(arg interface{}) {
	fmt.Println("num type is", reflect.TypeOf(arg))
	fmt.Println("num value is", reflect.ValueOf(arg))
}

type User struct {
	// 注意:私有字段(小写字母开头)不能通过反射的 Interface() 方法获取值。
	Id   int
	Name string
	Tel  int
	Age  int
}

func (this *User) Call() {
	fmt.Println(this.Tel, "called...")
}

// panic: reflect.Value.Interface: cannot return value obtained from unexported field or method
// 遇到以上错误请检查 User 的字段
func GetInformation(input interface{}) {
	fmt.Println("\n-----获取Type-----")
	inputType := reflect.TypeOf(input)
	fmt.Println("inputType is", inputType.Name())

	fmt.Println("\n-----获取Value-----")
	inputValue := reflect.ValueOf(input)
	fmt.Println("inputValue is", inputValue)

	// 通过Type,获取字段
	fmt.Println("\n-----获取字段-----")
	for i := 0; i < inputType.NumField(); i++ {
		field := inputType.Field(i)
		value := inputValue.Field(i).Interface()
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 通过Type,获取方法
	// 此处获取方法的结果为空,是正常的
	// 因为 Call 方法定义在 *User 上,但调用 GetInformation(Bob) 时传的 User
	// 解决方法:Call 方法改为定义在 User 上,或者 GetInformation 传 User*
	fmt.Println("\n-----获取方法-----")
	for i := 0; i < inputType.NumMethod(); i++ {
		method := inputType.Method(i)
		fmt.Printf("%s: %v\n", method.Name, method.Type)
	}
}

func main() {
	// 已知类型反射
	fmt.Println("========已知类型=========")
	var num float32 = 1.678
	ReflectNum(num)

	// 未知类型反射
	fmt.Println("\n========未知类型=========")
	Bob := User{1, "Bob", 665544, 19}
	GetInformation(Bob)
}

5. 结构体标签

结构体标签为字段提供元数据,常用于序列化等场景。

Go 复制代码
// 标签起一个解释说明的作用,使用反引号"包裹
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	// 多个标签中间使用空格相连
	// 标签的 key 和 value 是基于双方约定的。(key 已知,value 未知)
	ID    int    `json:"id" xml:"id"`
	Name  string `json:"name" xml:"name,attr"`
	Email string `json:"email,omitempty" validate:"required,email"`
}

func GetTag(arg interface{}) {
	// .Elem() 用于获取指针指向的元素类型
	elem := reflect.TypeOf(arg).Elem()
	for i := 0; i < elem.NumField(); i++ {
		tagJson := elem.Field(i).Tag.Get("json")
		tagXml := elem.Field(i).Tag.Get("xml")
		fmt.Println("json:", tagJson, ",xml:", tagXml)
	}
}
func main() {
	var user User
	GetTag(&user)
}

JSON序列化

结构体标签在实际开发中最常见的应用就是JSON序列化。

Go 复制代码
package main

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	ID     int      `json:"id" xml:"id"`
	Name   string   `json:"name" xml:"name,attr"`
	Actors []string `json:"actors"`
}

func main() {
	movie := Movie{1, "哪吒之魔童闹海", []string{"哪吒", "敖丙"}}

	// 编码:结构体------>json
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		// 如果错误信息有返回值,说明出错了
		fmt.Println("JSON marshal error:", err)
		return
	}
	fmt.Printf("JSON 字符串: %s\n", jsonStr)

	// 解码:json------>结构体
	var myMovie Movie
	err = json.Unmarshal(jsonStr, &myMovie)
	if err != nil {
		fmt.Println("JSON unmarshal error:", err)
		return
	}
	fmt.Printf("结构体: %v\n", myMovie)
}

如有问题或建议,欢迎在评论区中留言~

相关推荐
没有感情的robot4 小时前
使用ffmpeg裁剪视频
1024程序员节
eguid_14 小时前
【HLS】Java实现统计HLS的m3u8清单中所有ts切片的视频持续时长
java·音视频·hls·1024程序员节·m3u8·ts时长
snpgroupcn4 小时前
INEOS 能源携手 SNP 完成 SAP ECC 至 S/4HANA 战略升级 2024 年 10 月英国上线
1024程序员节
接着奏乐接着舞4 小时前
react nextjs 项目部署
1024程序员节
皓月Code4 小时前
第三章、React项目国际化介绍(`react-i18next`)
前端·javascript·react.js·1024程序员节
小苏兮4 小时前
【数据结构】二叉搜索树
开发语言·数据结构·c++·学习·1024程序员节
清风6666665 小时前
基于单片机的鱼缸监测与远程管理系统设计
单片机·毕业设计·课程设计·1024程序员节·期末大作业
梦凡尘5 小时前
Three.js 实现 3d 面积图
1024程序员节
2301_764441335 小时前
身份证校验工具
前端·python·1024程序员节