目录
一、结构体
结构体定义
结构体(struct)是一种复合数据类型,它允许您定义一组具有相关性的字段(field),每个字段都有自己的类型和名称。结构体用于表示具有多个属性的复杂数据结构,如用户信息、订单详情。
type User struct {
ID int
Name string
Email string
Age uint8
IsActive bool
}
使用指针接收者来操作结构体实例,这样可以避免结构体的值复制,有利于性能优化和共享资源的同步
func (u *User) UpdateEmail(newEmail string) {
u.Email = newEmail
}
package main
import "fmt"
// Student 定义结构体
type Student struct {
Name string
Age int
}
// PrintInfo 给机构体绑定一个方法
func (s Student) PrintInfo() {
fmt.Printf("name:%s age:%d\n", s.Name, s.Age)
}
func main() {
s := Student{
Name: "os",
Age: 21,
}
s.Name = "lee" // 修改值
s.PrintInfo()
}
继承
Go语言本身并不直接支持类(class)和传统的面向对象继承机制,如子类继承父类的属性和方法。Go语言采用组合(composition)和接口(interface)来实现类似继承的功能。
以下是Go语言中实现“继承”效果的两种主要方式:
组合(Composition)
组合是通过在新类型中包含已有的类型(通常是结构体)作为其字段来实现的。这样,新类型就“继承”了原有类型的全部字段和方法。
package main
import "fmt"
type Animal struct {
Name string
Age int
}
type Dog struct {
Animal // 匿名字段,相当于 Dog 内含一个 Animal 类型的字段
Breed string
}
func (a *Animal) Speak() {
fmt.Printf("%s makes a sound.\n", a.Name)
}
// 在这个例子中,Dog类型通过包含一个匿名的Animal字段,
// 获得了Animal的所有字段(Name和Age)和方法(Speak)。
// Dog类型还可以添加自己的字段(如Breed)和方法,从而实现类似继承的效果。
func main() {
d := Dog{
Animal: Animal{Name: "Rex", Age: 3},
Breed: "Golden Retriever",
}
d.Speak() // 输出: Rex makes a sound.
}
接口(Interfaces)
接口定义了一组方法签名,任何类型只要实现了接口中定义的所有方法,就被认为实现了该接口。通过接口,不同类型可以共享一套公共方法集,实现类似面向对象多态的效果。
package main
import "fmt"
type Speaker interface {
Speak()
}
type Cat struct {
Name string
}
func (c Cat) Speak() {
fmt.Printf("%s says 'Meow'.\n", c.Name)
}
type Parrot struct {
Name string
}
func (p Parrot) Speak() {
fmt.Printf("%s says 'Polly wanna cracker?'.\n", p.Name)
}
func MakeThemSpeak(speaker Speaker) {
speaker.Speak()
}
// 在这个例子中,Cat和Parrot类型都实现了Speaker接口,即它们都具有Speak方法。
// MakeThemSpeak函数接受Speaker接口作为参数,因此可以接受任何实现了Speaker接口的类型实例。
// 通过接口,Cat和Parrot可以共享Speaker的行为,实现了某种程度上的“继承”。
func main() {
cat := Cat{Name: "Whiskers"}
parrot := Parrot{Name: "Polly"}
MakeThemSpeak(cat) // 输出: Whiskers says 'Meow'.
MakeThemSpeak(parrot) // 输出: Polly says 'Polly wanna cracker?'.
}
总结来说,Go语言通过组合和接口这两种机制,鼓励开发者采用“组合优于继承”的原则来设计软件,避免了复杂的继承层次和类型之间的紧密耦合。通过合理的组合和接口设计,Go语言程序可以实现灵活、模块化的代码结构。
结构体指针
结构体指针是一种特殊的数据类型,它存储的是一个结构体变量的内存地址。使用结构体指针可以更高效地操作结构体,特别是在涉及大结构体或需要修改结构体内容的场景。
结构体标签
结构体(struct)字段的标签(tag)是一种附加在字段定义后面的元数据,主要用于与第三方库(如序列化库、数据库驱动等)交互,提供额外的信息或指令。
结构体标签是Go语言中实现与外部库或工具交互的重要手段,通过在结构体字段上添加适当的标签,可以定制化数据的序列化、反序列化、数据库映射、验证规则等行为。
结构体标签的常见用途包括:
序列化与反序列化
JSON 库(encoding/json
)使用 json
标签来控制字段的序列化和反序列化行为,如字段名映射、忽略字段等。
// json:"id" 指定了在JSON中,ID 字段映射为 "id";
// json:"name,omitempty" 表示在序列化时,如果 Name 字段值为空,则不包含该字段;
type User struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
}
数据库操作
ORM 库(如
sqlx
、gorm
等)使用db
、sql
或自定义标签(如gorm
的gorm:
)来映射数据库字段、设置约束条件等。
// db:"id" 表示 ID 字段对应数据库中的 "id" 字段。
// sql:",notnull" 指定 Name 字段在数据库中不允许为 NULL。
type User struct {
ID int `db:"id"`
Name string `sql:",notnull"`
}
验证库
验证库(如
validator/v10
)使用特定标签(如validate:"required"
)来定义字段的验证规则。
// validate:"required,email" 表示 Email 字段必须存在(required)
// 且必须符合电子邮件格式(email)。
type User struct {
Email string `validate:"required,email"`
}
如何获取结构体标签
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"user_name"`
}
func main() {
// 创建一个User实例
u := User{ID: 1, Name: "Alice"}
// 获取User类型反射值
userValue := reflect.ValueOf(u)
userType := userValue.Type()
// 遍历User的字段
for i := 0; i < userType.NumField(); i++ {
field := userType.Field(i)
// 获取字段名
fieldName := field.Name
fmt.Printf("Field Name: %s\n", fieldName)
// 获取字段标签(假设我们关心的是json和db标签)
jsonTag := field.Tag.Get("json")
dbTag := field.Tag.Get("db")
fmt.Printf("json Tag: %s\n", jsonTag)
fmt.Printf("db Tag: %s\n", dbTag)
// 输出标签值或默认值(如果不存在)
if jsonTag != "" {
fmt.Printf("JSON mapping: %s -> %s\n", fieldName, jsonTag)
} else {
fmt.Printf("No JSON tag for field %s\n", fieldName)
}
if dbTag != "" {
fmt.Printf("DB mapping: %s -> %s\n", fieldName, dbTag)
} else {
fmt.Printf("No DB tag for field %s\n", fieldName)
}
fmt.Println()
}
}
二、自定义数据类型
自定义类型指的是使用 type 关键字定义的新类型,它可以是基本类型的别名,也可以是结构体、函数等组合而成的新类型。自定义类型可以帮助我们更好地抽象和封装数据,让代码更加易读、易懂、易维护
自定义类型
结构体就是自定义类型中的一种
除此之外我们使用自定义类型,还可以让代码组合更加规范
例如,响应给客户端的想要码,我给他一个自定义类型
package main
import "fmt"
type Code int
const (
SuccessCode Code = 0
ValidCode Code = 7 // 校验失败的错误
ServiceErrCode Code = 8 // 服务错误
)
func (c Code) GetMsg() string {
// 可能会有更加响应码返回不同消息内容的要求,我们在这个函数里面去实现即可
// 可能还会有国际化操作
return "成功"
}
func main() {
fmt.Println(SuccessCode.GetMsg())
var i int
fmt.Println(int(SuccessCode) == i) // 必须要转成原始类型才能判断
}
类型别名
和自定义类型很像,但是有一些地方和自定义类型有很大差异
- 不能绑定方法
- 打印类型还是原始类型
- 和原始类型比较,类型别名不用转换
package main
import "fmt"
type AliasCode = int
type MyCode int
const (
SuccessCode MyCode = 0
SuccessAliasCode AliasCode = 0
)
// MyCodeMethod 自定义类型可以绑定自定义方法
func (m MyCode) MyCodeMethod() {
}
// MyAliasCodeMethod 类型别名 不可以绑定方法
func (m AliasCode) MyAliasCodeMethod() {
}
func main() {
// 类型别名,打印它的类型还是原始类型
fmt.Printf("%T %T \n", SuccessCode, SuccessAliasCode) // main.MyCode int
// 可以直接和原始类型比较
var i int
fmt.Println(SuccessAliasCode == i)
fmt.Println(int(SuccessCode) == i) // 必须转换之后才能和原始类型比较
}
三、接口
接口定义
接口是一组仅包含方法名、参数、返回值的未具体实现的方法的集合
package main
import "fmt"
// Animal 定义一个animal的接口,它有唱,跳,rap的方法
type Animal interface {
sing()
jump()
rap()
}
// Chicken 需要全部实现这些接口
type Chicken struct {
Name string
}
func (c Chicken) sing() {
fmt.Println("chicken 唱")
}
func (c Chicken) jump() {
fmt.Println("chicken 跳")
}
func (c Chicken) rap() {
fmt.Println("chicken rap")
}
// 全部实现完之后,chicken就不再是一只普通的鸡了
func main() {
var animal Animal
animal = Chicken{"ik"}
animal.sing()
animal.jump()
animal.rap()
}
如何实现接口?
一个类型实现了接口的所有方法,即实现了该接口
类型断言
在使用接口的时候,我还是希望知道此是的具体类型是什么,我们可以通过断言来获取
func sing(obj Animal) {
// 通过断言来获取此时的具体类型
switch obj.(type) {
case Chicken:
fmt.Println("鸡")
case Cat:
fmt.Println("猫")
}
obj.sing()
}
或者是断言某个类型
func sing(obj Animal) {
c, ok := obj.(Chicken) // 两个参数 断言之后的类型 是否是对应类型
fmt.Println(c, ok)
d := obj.(Cat) // 一个参数 就是断言之后的类型,注意,类型不对是要报错的 main.Animal is main.Chicken, not main.Cat
fmt.Println(d)
}
空接口
空接口(Empty Interface)是一种特殊的接口类型,它没有任何方法定义。空接口的定义如下:
interface{}
空接口有两个显著特点:
任何类型都实现了空接口: 由于空接口没有定义任何方法,所以任何类型(无论是基本类型、复合类型、自定义类型,甚至是其他接口类型)都隐式地实现了空接口。这意味着任何类型的值都可以赋值给空接口类型的变量。
作为通用容器: 由于任何类型都实现了空接口,空接口类型变量可以存储任何类型的值。因此,空接口常被用作通用的数据容器,用于在不知道确切类型的情况下处理多种类型的值。
空接口在Go语言中的常见应用场景包括:
函数参数:函数可以接受空接口类型的参数,以接收任意类型的输入。这样,函数可以处理多种不同类型的值,实现通用的处理逻辑。
func printValue(value interface{}) {
fmt.Printf("Received value of type %T: %v\n", value, value)
}
printValue("Hello") // 输出:Received value of type string: Hello
printValue(42) // 输出:Received value of type int: 42
printValue(true) // 输出:Received value of type bool: true
类型断言:在持有空接口类型变量时,可以通过类型断言来检查并转换其实际类型,以便进行类型相关的操作。
var anyValue interface{} = "Gopher"
strValue, ok := anyValue.(string)
if ok {
fmt.Println("String value:", strValue)
} else {
fmt.Println("Not a string")
}
接口类型转换:空接口可以作为其他接口类型的中间类型,用于转换不同接口类型的值。
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type WriteCloser interface {
Writer
Closer
}
var any interface{} = &os.File{} // *os.File implements both Writer and Closer
wc, ok := any.(WriteCloser)
if ok {
// wc is now a WriteCloser
}
反射:空接口常与反射(reflect
包)结合使用,用于在运行时动态检查和操作未知类型的值。
总的来说,Go语言中的空接口提供了类型无关的编程能力,使得代码能够在不了解具体类型的情况下处理多种类型的值。然而,过度使用空接口可能会牺牲类型安全性,因此在实际编程中应结合具体需求,谨慎使用并结合类型断言、类型开关(type switch)或反射等技术,确保代码的清晰性和可维护性。