Golang教程三(结构体、自定义数据类型,接口)

目录

一、结构体

结构体定义

 继承

组合(Composition)

 接口(Interfaces)

结构体指针

 结构体标签

序列化与反序列化

 数据库操作

 验证库

 如何获取结构体标签

二、自定义数据类型

自定义类型

 类型别名

 三、接口

接口定义

类型断言

空接口


一、结构体

结构体定义

结构体(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 库(如 sqlxgorm 等)使用 dbsql 或自定义标签(如 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) // 必须要转成原始类型才能判断
}

 类型别名

和自定义类型很像,但是有一些地方和自定义类型有很大差异

  1. 不能绑定方法
  2. 打印类型还是原始类型
  3. 和原始类型比较,类型别名不用转换
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{}

空接口有两个显著特点: 

  1. 任何类型都实现了空接口: 由于空接口没有定义任何方法,所以任何类型(无论是基本类型、复合类型、自定义类型,甚至是其他接口类型)都隐式地实现了空接口。这意味着任何类型的值都可以赋值给空接口类型的变量。

  2. 作为通用容器: 由于任何类型都实现了空接口,空接口类型变量可以存储任何类型的值。因此,空接口常被用作通用的数据容器,用于在不知道确切类型的情况下处理多种类型的值。

空接口在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)或反射等技术,确保代码的清晰性和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值