本专栏文章持续更新,新增内容使用蓝色表示。
食用指南
本文适合有 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)
}

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