在Go语言中,封装是通过使用结构体(structs)和方法(methods)来实现的。Go语言的封装不同于传统面向对象编程语言中的类(class)系统,但它提供了类似的功能。封装在Go中主要体现在控制对结构体字段的访问权限上。
结构体是组合相关属性的方式。你可以将数据封装在一个结构体内。
type person struct {
name string
age int
}
在这个例子中,person
结构体封装了 name
和 age
字段。
type Person struct {
Name string // 导出字段
age int // 非导出字段
}
在这个例子中,Name
是一个导出字段,可以被包外的代码访问,而 age
是非导出的,只能在定义它的包内部访问。
通过定义方法(函数与特定类型关联),你可以控制结构体的行为,以及如何访问和修改其内部状态。
func (p *Person) SetAge(age int) {
p.age = age
}
func (p *Person) Age() int {
return p.age
}
在这个例子中,SetAge
和 Age
方法允许外部代码以受控的方式访问和修改私有字段 age
。
package main
import (
"fmt"
)
// Person 结构体,封装了个人的详细信息
type Person struct {
Name string // 公开字段
age int // 私有字段
}
// NewPerson 是一个构造函数,返回一个新的Person实例
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
age: age,
}
}
// SetAge 方法设置Person的年龄
func (this *Person) SetAge(age int) {
if age > 0 {
this.age = age
}
}
// GetAge 方法返回Person的年龄
func (this *Person) GetAge() int {
return this.age
}
func main() {
// 创建一个Person实例
person := NewPerson("Alice", 30)
// 访问并打印Person的公开字段
fmt.Println("Name:", person.Name)
// 访问私有字段通过方法
fmt.Println("Age:", person.GetAge())
// 设置新的年龄
person.SetAge(31)
fmt.Println("New Age:", person.GetAge())
}
输出:
Name: Alice
Age: 30
New Age: 31
在Go语言中,接收器(receiver)是定义在方法中的一个特殊参数,它指定了该方法绑定到哪种类型上。这允许你在特定类型的实例上调用该方法,类似于其他面向对象语言中的方法。在Go中,接收器可以是该类型的值或该类型的指针。
值接收器使用类型的值来调用方法。当使用值接收器时,方法在调用时会接收调用对象的一个副本。
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
在这个例子中,SayHello
方法通过值接收器绑定到 Person
类型上。当你调用这个方法时,p
是 Person
实例的一个副本。
指针接收器使用指向类型的指针来调用方法。这意味着方法可以修改接收器指向的值。
func (p *Person) SetAge(newAge int) {
p.Age = newAge
}
在这里,SetAge
方法使用指针接收器,所以它可以修改 Person
实例的 Age
字段。
选择使用值接收器还是指针接收器主要取决于以下几点:
接收器允许你将函数与特定类型的实例关联,类似于面向对象编程中的方法。这样,你可以对这个类型的每个实例调用它的方法,就像是这个类型自己的行为一样。接收器是Go语言实现面向对象编程风格的关键元素之一。
虽然Go语言没有像Python或C++中那样的类继承概念,但它使用结构体嵌入和组合来实现类似继承的功能。对于熟悉Python和C++的开发者来说,理解Go的这种继承方式需要转换一下思维方式。
在Go中,继承是通过在一个结构体中嵌入另一个结构体来实现的。这种方法被称为组合(Composition)。嵌入的结构体的方法和字段可以被外层结构体直接访问,就好像它们是外层结构体的一部分。
假设我们有一个基本的 Animal
结构体,然后我们想要创建一个 Dog
结构体,它拥有 Animal
的所有特性,并添加一些自己的特性。
type Animal struct {
Name string
}
func (a *Animal) Speak() {
fmt.Println(a.Name, "makes a sound")
}
type Dog struct {
Animal // 嵌入Animal
Breed string
}
func (d *Dog) Speak() {
fmt.Println(d.Name, "barks")
}
在这个例子中,Dog
嵌入了 Animal
。这意味着 Dog
自动获得了 Animal
的所有字段和方法。我们还可以为 Dog
添加新的方法或重写(Override)继承的方法,就像上面的 Speak
方法那样。
当调用一个嵌入结构体的方法时,如果外层结构体没有同名方法,则会调用嵌入结构体的方法。
func main() {
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Shepherd"}
d.Speak() // 输出: Rex barks
d.Animal.Speak() // 输出: Rex makes a sound
}
在这里,调用 d.Speak()
时,由于 Dog
结构体有自己的 Speak
方法,所以调用的是 Dog
的方法。而 d.Animal.Speak()
则显式地调用了 Animal
的 Speak
方法。
super
函数调用基类的方法。super
关键字,但可以通过直接调用嵌入结构体的方法来实现类似的行为。在Go语言中,接口和多态是实现灵活、可扩展代码的核心概念。Go的接口提供了一种声明行为的方式,而多态则允许你以统一的方式使用实现了相同接口的不同类型。
接口在Go中是一组方法签名的集合。当一个类型提供了接口中所有方法的实现,它就被认为实现了该接口。这种实现是隐式的,不需要像Java或C#中那样显式声明。
type Shape interface {
Area() float64
Perimeter() float64
}
这个 Shape
接口定义了两个方法:Area
和 Perimeter
。任何提供了这两个方法的类型都隐式地实现了 Shape
接口。
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
在这里,Rectangle
类型提供了 Shape
接口要求的 Area
和 Perimeter
方法,因此它实现了 Shape
接口。
多态是面向对象编程中的一个核心概念,它允许你以统一的方式处理不同类型的实例。在Go中,多态是通过接口实现的。
你可以编写接受接口类型参数的函数或方法,然后用实现了该接口的任何类型的实例调用它。
func PrintShapeInfo(s Shape) {
fmt.Println("Area:", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
}
func main() {
r := Rectangle{Width: 10, Height: 5}
PrintShapeInfo(r)
}
在 PrintShapeInfo
函数中,参数 s
是 Shape
接口类型。这意味着你可以传递任何实现了 Shape
接口的类型(如 Rectangle
)的实例。
下面是一个完整的Go程序示例,展示了之前讨论的接口和多态概念。这个程序定义了一个 Animal
接口,以及几种实现了这个接口的动物类型。程序中还包含了一个演示多态性的函数和 main
函数,用于运行程序。
package main
import (
"fmt"
)
// Animal 接口定义了一个方法 Speak
type Animal interface {
Speak() string
}
// Dog 类型实现了 Animal 接口
type Dog struct{}
func (d *Dog) Speak() string {
return "Woof!"
}
// Cat 类型实现了 Animal 接口
type Cat struct{}
func (c *Cat) Speak() string {
return "Meow!"
}
// Cow 类型实现了 Animal 接口
type Cow struct{}
func (cow *Cow) Speak() string {
return "Moo!"
}
// AnimalSpeak 接收 Animal 接口类型的参数
// 并调用其 Speak 方法
func AnimalSpeak(a Animal) {
fmt.Println(a.Speak())
}
// main 函数是程序的入口
func main() {
animals := []Animal{&Dog{}, &Cat{}, &Cow{}}
for _, animal := range animals {
AnimalSpeak(animal)
}
}
在这个程序中:
Animal
接口,该接口有一个方法 Speak
。Dog
、Cat
和 Cow
类型分别实现了 Animal
接口。AnimalSpeak
函数接受任何实现了 Animal
接口的类型,并调用其 Speak
方法。main
函数中,我们创建了一个包含不同动物的切片,并对每个动物调用了 AnimalSpeak
函数。在Go语言中,空接口(interface{}
)是一个特殊的类型,它不包含任何方法声明。由于Go中的接口是隐式实现的,任何类型都至少实现了零个方法,因此可以说所有类型都实现了空接口。这使得空接口在Go中具有独特的灵活性和用途。
相当于
Java
中的Object
空接口表示为 interface{}
,它可以包含任何类型的值:
由于空接口没有定义任何方法,因此任何类型的值都可以被赋给空接口变量。
由于空接口可以包含任何类型,因此它常被用作存储不确定类型值的通用容器。
var anything interface{}
anything = "Hello, World"
anything = 123
anything = []int{1, 2, 3}
空接口允许函数接受任意类型的参数或返回任意类型的数据。
func PrintValue(v interface{}) {
fmt.Println(v)
}
你可以使用类型断言来提取空接口中的实际类型。
// 判断any是否为string
if str, ok := anything.(string); ok {
fmt.Println(str)
}
当 anything
变量实际上是一个 string
类型时:
str, ok := anything.(string)
会成功执行。str
将获得 anything
变量的值,因为 anything
确实是一个 string
类型。ok
变量将被设置为 true
,表示类型断言成功。例如,如果 anything := "hello"
,那么在执行类型断言后,str
将是 "hello"
,ok
将是 true
。
当 anything
变量不是一个 string
类型时:
str, ok := anything.(string)
不会导致程序崩溃,而是安全地返回两个值。str
将被赋予 string
类型的零值,即空字符串 ""
。这是因为类型断言失败,str
无法获得 anything
的值。ok
变量将被设置为 false
,表示类型断言没有成功。例如,如果 anything := 42
(一个 int
类型),那么在执行类型断言后,str
将是 ""
(空字符串),ok
将是 false
。
在诸如切片、映射等数据结构中,空接口可以用来存储各种不同类型的数据。
var mixedSlice []interface{}
mixedSlice = append(mixedSlice, 42, "foo", true)
Go语言中的反射是一个强大且复杂的功能,它允许程序在运行时检查、修改变量的类型和值,以及调用其关联的方法。反射主要由 reflect
包提供支持。理解反射的关键在于理解Go中的类型(Type)和值(Value)的概念。
在Go的 reflect
包中,主要有两个重要的类型:reflect.Type
和 reflect.Value
。
var x float64 = 3.4
t := reflect.TypeOf(x)
fmt.Println("Type:", t.Name()) // 输出: Type: float64
这里,reflect.TypeOf
函数用于获取变量 x
的类型信息。
v := reflect.ValueOf(x)
fmt.Println("Value:", v.Float()) // 输出: Value: 3.4
reflect.ValueOf
函数返回了一个 reflect.Value
类型的值,代表了变量 x
的值。
要修改一个值,你需要确保这个值是可设置的(Settable)。通常这意味着你需要传递一个变量的指针给 reflect.ValueOf
。
var y float64 = 3.4
p := reflect.ValueOf(&y) // 注意:这里传递的是指针
v := p.Elem()
v.SetFloat(7.1)
fmt.Println(y) // 输出: 7.1
这里,p.Elem()
提供了一个指针指向的值的 reflect.Value
,这个值是可以设置的。
在Go语言中,使用反射深度遍历复杂类型(如结构体中嵌套的结构体或数组)可以是一个复杂的任务。但是,通过 reflect
包的功能,我们可以递归地访问和操作这些复杂类型的每个字段。以下是一个示例,展示了如何使用反射来深度遍历一个结构体,包括它的嵌套字段:
package main
import (
"fmt"
"reflect"
)
// 遍历结构体的函数
func walk(v reflect.Value, depth int) {
// 获取v的类型
typ := v.Type()
switch v.Kind() {
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := typ.Field(i)
fmt.Printf("%*s%s (%s): ", depth*2, "", fieldType.Name, field.Type())
if field.CanInterface() {
fmt.Println(field.Interface())
} else {
fmt.Println("unexported field")
}
// 递归调用walk
walk(field, depth+1)
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
fmt.Printf("%*s%d: ", depth*2, "", i)
walk(v.Index(i), depth+1)
}
}
}
type Person struct {
Name string
Age int
Friends []string
Parent *Person
}
func main() {
james := Person{
Name: "James",
Age: 35,
Friends: []string{"David", "Emma"},
Parent: &Person{Name: "John"},
}
walk(reflect.ValueOf(james), 0)
}
walk
的函数,它使用递归来遍历结构体的每个字段。reflect.Value
和 reflect.Type
来获取关于值和类型的信息,并通过 Field
方法访问结构体的字段。CanInterface
和 Interface
方法获取其值。depth
参数用于在打印时生成缩进,使输出更容易阅读。CanInterface
方法用于检查是否可以安全地调用 Interface
方法。对于非导出(私有)字段,CanInterface
会返回 false
,此时调用 Interface
会引发panic。package main
import (
"fmt"
"reflect")
type User struct {
Id int
Name string
Age int
}
func (this *User) Call() {
fmt.Println("user is called ...")
fmt.Printf("%v\n\n", this)
}
func main() {
user := User{1, "Chance", 18}
DoFiledAndMethod(user)
fmt.Println("--------------------------------------------------")
DoFiledAndMethodOnPtr(&user)
}
func DoFiledAndMethod(obj interface{}) {
// 获取obj的type
Type := reflect.TypeOf(obj)
fmt.Println("inputType is ", Type.Name())
// 获取obj的value
Value := reflect.ValueOf(obj)
fmt.Println("inputValue is ", Value)
//获取type里面的字段
for i := 0; i < Type.NumField(); i++ {
field := Type.Field(i)
value := Value.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
//通过type里面的方法
for i := 0; i < Type.NumMethod(); i++ {
method := Type.Method(i)
fmt.Printf("%s: %v\n", method.Name, method.Type)
}
}
func DoFiledAndMethodOnPtr(input interface{}) {
Type := reflect.TypeOf(input)
Value := reflect.ValueOf(input)
// If pointer get the underlying element≤
if Type.Kind() == reflect.Ptr {
Type = Type.Elem()
Value = Value.Elem()
}
fmt.Println("inputType is ", Type.Name())
fmt.Println("inputValue is ", Value)
for i := 0; i < Type.NumField(); i++ {
field := Type.Field(i)
value := Value.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
for i := 0; i < Type.NumMethod(); i++ {
method := Type.Method(i)
fmt.Printf("%s: %v\n", method.Name, method.Type)
}
}
输出:
inputType is User
inputValue is {1 Chance 18}
Id: int = 1
Name: string = Chance
Age: int = 18
Call: func(main.User)
--------------------------------------------------
inputType is User
inputValue is {1 Chance 18}
Id: int = 1
Name: string = Chance
Age: int = 18
Call: func(main.User)
在Go语言中,结构体字段可以通过标签(Tag)附加元数据。这些标签不会直接影响程序的行为,但可以通过反射在运行时读取,常用于序列化/反序列化、表单验证、ORM映射等场景。
结构体标签是定义在结构体字段后的字符串字面量,通常由一系列键值对组成,键和值之间使用冒号分隔,多个键值对之间用空格分隔。
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age" validate:"min=18"`
}
在这个例子中,User
结构体的每个字段都有一个 json
标签,用于定义JSON序列化时该字段的名称。Age
字段还有一个额外的 validate
标签,可能被用于字段验证。
要读取标签,你需要使用 reflect
包。以下是一个示例,演示如何读取结构体字段的标签:
func PrintTags(u User) {
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s, Tag: '%s'\n", field.Name, field.Tag)
}
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
PrintTags(user)
}
这段代码使用反射来遍历 User
类型的字段,并打印出每个字段的名称和标签。
encoding/json
包,可以控制结构体如何被转换为JSON,或从JSON转换回来。json
、xml
、gorm
等库都有自己的标签格式。结构体标签在Go中是一种强大的机制,用于提供关于结构体字段的额外信息,尤其在数据序列化和ORM映射等方面非常有用。
package main
import (
"encoding/json"
"fmt")
type Movie struct {
Title string `json:"title"` // 通过指定tag实现json序列化该字段时的key
Year int `json:"year"` // 同上
Price int `json:"price"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{
Title: "喜剧之王",
Year: 2000,
Price: 10,
Actors: []string{"周星驰", "张柏芝"},
}
jsonStr, err := json.Marshal(movie) // 返回byte切片
if err != nil {
fmt.Println("json marshal error", err)
return
}
fmt.Printf("%s\n", jsonStr)
inputMovie := Movie{}
err = json.Unmarshal(jsonStr, &inputMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Printf("%v\n", inputMovie)
}
输出:
{"title":"喜剧之王","year":2000,"price":10,"actors":["周星驰","张柏芝"]}
{喜剧之王 2000 10 [周星驰 张柏芝]}