目录
[Rune 类型](#Rune 类型)
[Byte 类型](#Byte 类型)
[1. 接收者类型](#1. 接收者类型)
[2. 方法定义](#2. 方法定义)
[3. 值接收者 vs 指针接收者](#3. 值接收者 vs 指针接收者)
[4. 调用方式的灵活性](#4. 调用方式的灵活性)
[5. 方法集](#5. 方法集)
[6. 方法与继承](#6. 方法与继承)
[7. 方法声明的位置](#7. 方法声明的位置)
在Go语言中,基础类型(也称为原始类型)是构建更复杂数据结构的基石。Go语言的基础类型可以分为几个主要类别,包括数字类型、布尔类型、和字符串类型。下面,我将为您详细介绍这些类型及其声明方法,帮助您更好地理解和使用它们。
数字类型
在Go语言中,整数类型分为有符号和无符号,各自的范围也不同。这里给出一些整数类型的声明示例:
Go
var i int = 1
var i8 int8 = 127
var i16 int16 = 32767
var i32 int32 = -2147483648
var i64 int64 = 9223372036854775807
var u uint = 1
var u8 uint8 = 255
var u16 uint16 = 65535
var u32 uint32 = 4294967295
var u64 uint64 = 18446744073709551615
var f32 float32 = 3.1415926
var f64 float64 = 3.1414926564565
布尔类型
布尔类型表示逻辑值,只有true
和false
两个值。
Go
var b bool = true
if b {
fmt.Println("b 是真")
} else {
fmt.Println("b 是假")
}
字符串类型
Go
var s string = "Hello, Go!" // 字符串类型
Rune 类型
rune
类型代表一个Unicode码点。它是int32
的别名,用于处理Unicode字符。
Go
var r rune = '中' // 使用单引号声明rune类型,代表一个Unicode字符
fmt.Printf("%T, %d\n", r, r) // 输出: int32, 20013('中'的Unicode码点)
Byte 类型
byte
类型是uint8
的别名,常用于处理ASCII字符或字节数据。
Go
var b byte = 'A' // 使用单引号声明byte类型,代表一个ASCII字符
fmt.Printf("%T, %d\n", b, b) // 输出: uint8, 65('A'的ASCII码)
类型转换
Go语言中的类型转换需要显式进行。这是因为Go语言在设计时强调类型安全,避免了隐式类型转换可能带来的问题。
Go
var i int = 42
var f float64 = float64(i) // 将int类型显式转换为float64类型
var u uint = uint(f) // 将float64类型显式转换为uint类型
fmt.Println(i, f, u) // 输出: 42 42 42
结构体
结构体定义
Go
type Person struct {
name string
Age int
}
结构体作为函数参数
Go
type Person struct {
name string
Age int
}
func testPersion(p1 Person) Person {
var p Person
p.name = "Alice"
p.Age = 18
return p
}
结构体方法
Go
package main
import "fmt"
type Circle struct {
Radius float64
}
// Circle类型的方法,计算圆的面积
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
circle := Circle{Radius: 5}
fmt.Println("Circle area:", circle.Area()) // 输出:Circle area: 78.5
}
1. 接收者类型
- 值接收者:方法通过值接收其所属类型的一个副本。这意味着方法内部对接收者的任何修改都不会影响原始实例。
- 指针接收者:方法通过指针接收其所属类型,允许直接修改原始实例的字段。使用指针接收者还可以避免在调用方法时复制大型结构体,从而提高效率。
2. 方法定义
方法定义与函数类似,但在函数名之前添加了一个额外的参数,称为接收者,表示该方法附加到的类型。
3. 值接收者 vs 指针接收者
- 使用值接收者定义的方法在调用时,接收者会被复制一份。这适用于小型结构体或你不希望方法能修改其接收者的场景。
- 使用指针接收者可以让方法修改其接收者指向的值。这对于需要修改接收者,或避免在每次方法调用时复制大型结构体时非常有用。
4. 调用方式的灵活性
- 即使方法的接收者是值类型,你也可以使用指针来调用该方法,Go语言会自动解引用。
- 同样,如果方法的接收者是指针类型,你也可以直接使用值类型调用该方法,Go语言会自动取地址。
这个特性使得Go语言在处理方法调用时非常灵活。下面是一个示例,展示了如何定义和使用值接收者和指针接收者的方法,以及如何通过值类型和指针类型来调用这些方法。
Go
package main
import "fmt"
type MyStruct struct {
Value int
}
// 使用值接收者定义的方法
func (s MyStruct) ValueReceiverMethod() {
fmt.Printf("ValueReceiverMethod 被调用, Value: %d\n", s.Value)
}
// 使用指针接收者定义的方法
func (s *MyStruct) PointerReceiverMethod() {
fmt.Printf("PointerReceiverMethod 被调用, Value: %d\n", s.Value)
}
func main() {
value := MyStruct{Value: 5}
// 通过值类型调用值接收者的方法
value.ValueReceiverMethod()
// 通过值类型调用指针接收者的方法
// Go语言自动取value的地址,使其符合方法的接收者类型
value.PointerReceiverMethod()
pointer := &MyStruct{Value: 10}
// 通过指针类型调用值接收者的方法
// Go语言自动解引用pointer,使其符合方法的接收者类型
pointer.ValueReceiverMethod()
// 通过指针类型调用指针接收者的方法
pointer.PointerReceiverMethod()
}
在这个示例中,
MyStruct
定义了两个方法:ValueReceiverMethod
使用值接收者,而PointerReceiverMethod
使用指针接收者。
- 当
ValueReceiverMethod
通过结构体实例的值调用时,Go语言直接使用这个值。- 当
PointerReceiverMethod
通过结构体实例的值调用时,Go自动取这个值的地址,以适应方法的指针接收者。- 同样,当
ValueReceiverMethod
通过结构体的指针调用时,Go自动解引用这个指针,以适应方法的值接收者。PointerReceiverMethod
通过结构体的指针调用时,直接使用这个指针,因为它与方法的接收者类型匹配。
5. 方法集
- 值类型的变量可以调用值接收者和指针接收者的方法。
- 指针类型的变量也可以调用值接收者和指针接收者的方法。
- 但是,接口类型的变量只能调用它的方法集内的方法。值接收者方法属于类型的值和指针的方法集,而指针接收者方法只属于类型指针的方法集
代码定义:
Go
package main
import "fmt"
// Shaper 接口,包含一个方法 ShapeName
type Shaper interface {
ShapeName() string
}
// Square 结构体
type Square struct {
Name string
}
// Circle 结构体
type Circle struct {
Name string
}
// 使用值接收者实现 Shaper 接口的 ShapeName 方法
func (s Square) ShapeName() string {
return "Square: " + s.Name
}
// 使用指针接收者实现 Shaper 接口的 ShapeName 方法
func (c *Circle) ShapeName() string {
return "Circle: " + c.Name
}
Go
func main() {
square := Square{Name: "MySquare"}
circle := Circle{Name: "MyCircle"}
// 值类型的Square可以直接赋值给Shaper接口
var s Shaper = square
fmt.Println(s.ShapeName())
// 指针类型的Circle也可以赋值给Shaper接口
var c Shaper = &circle
fmt.Println(c.ShapeName())
// 注意: 直接使用值类型的Circle赋值给Shaper接口会引发编译错误
// var cError Shaper = circle
// fmt.Println(cError.ShapeName())
// 直接通过值类型调用指针接收者的方法是可以的,Go会自动取地址
fmt.Println(circle.ShapeName())
// 通过指针类型调用值接收者的方法也是可以的,Go会自动解引用
circlePtr := &square
fmt.Println(circlePtr.ShapeName())
}
Square
使用值接收者实现了Shaper
接口的ShapeName
方法,所以Square
的值和指针类型都可以赋值给Shaper
接口变量。Circle
使用指针接收者实现了Shaper
接口的ShapeName
方法,所以只有Circle
的指针类型可以赋值给Shaper
接口变量。如果尝试将Circle
的值类型直接赋值给接口变量,会引发编译错误,因为Circle
值类型的方法集不包括ShapeName
方法。- 这个例子展示了如何根据方法的接收者类型(值接收者或指针接收者)以及它们如何匹配接口的方法集,来决定是否可以将一个类型的实例赋值给接口。
通过这个例子,你应该能更清楚地理解方法集和接口之间的关系,以及它们是如何影响Go语言中类型的行为的。掌握这些概念对于高效使用Go语言是非常重要的。继续探索和实践,你将能更深入地理解Go语言的面向对象特性。
6. 方法与继承
Go语言没有提供传统的继承机制,但可以通过嵌入结构体来实现类似继承的效果。方法也可以通过这种方式被嵌入的结构体所继承。
在Go语言中,虽然没有类似于传统面向对象语言的继承机制,但通过结构体的嵌入(有时也称为组合)和接口,我们可以实现类似继承的功能,包括方法的重用。这种方式提供了更灵活和更安全的代码组织形式。下面是一个示例,演示如何通过嵌入结构体来实现方法的"继承"。
假设我们有一个Car
结构体,它有一个Drive
方法,然后我们希望ElectricCar
结构体"继承"Car
的方法。
Go
package main
import "fmt"
// Car 基础结构体
type Car struct {
Name string
}
// Car的Drive方法
func (c Car) Drive() {
fmt.Println(c.Name, "is driving.")
}
// ElectricCar 嵌入了Car
type ElectricCar struct {
Car // 嵌入Car结构体
BatteryLife int
}
func main() {
eCar := ElectricCar{
Car: Car{Name: "Tesla Model S"},
BatteryLife: 300,
}
// 通过ElectricCar直接调用Car的Drive方法
eCar.Drive()
// 也可以通过嵌入的Car结构体字段调用
eCar.Car.Drive()
// 访问ElectricCar的字段
fmt.Println("Battery life:", eCar.BatteryLife)
// 访问嵌入的Car结构体的字段
fmt.Println("Car name:", eCar.Name) // 直接访问,Go自动解析到eCar.Car.Name
fmt.Println("Car name:", eCar.Car.Name) // 显式访问
}
ElectricCar
通过嵌入Car
结构体,"继承"了Car
的方法和属性。这意味着,即使没有显式定义Drive
方法,ElectricCar
的实例也能调用Drive
方法。- 通过这种嵌入机制,我们可以很容易地重用
Car
的功能,同时为ElectricCar
添加特有的属性和方法,例如BatteryLife
属性。- 你可以通过嵌入结构体的实例直接访问其公开的字段和方法,就像它们是外层结构体的一部分一样。在上面的示例中,
eCar.Name
实际上是对eCar.Car.Name
的访问简写。- 如果需要,嵌入的结构体也可以被显式地引用,如
eCar.Car.Drive()
。这种方式允许Go语言开发者在没有传统类继承的情况下实现非常灵活的类型设计。通过组合和接口,你可以构建出清晰、易于理解和维护的代码结构。这种方法强调的是,"有什么"(组合)而不是"是什么"(继承),更加注重对象的能力而非其身份。继续探索Go的类型系统,你会发现更多有趣且实用的设计模式。
7. 方法声明的位置
方法可以在类型定义内部或外部声明,但是接收者的类型必须在同一个包内。这意味着你不能为内置类型或来自其他包的类型定义方法。
为自定义类型声明方法
Go
package main
import "fmt"
// 在main包中定义了一个简单的结构体类型Person
type Person struct {
Name string
}
// 为Person类型声明一个方法SayHello,这是允许的,因为Person在同一个包内定义
func (p Person) SayHello() {
fmt.Println("Hello,", p.Name)
}
func main() {
person := Person{Name: "张三"}
person.SayHello() // 调用SayHello方法
}
在这个例子中,Person
类型和它的方法SayHello
都是在main
包中定义的。这完全符合Go语言的规则。
尝试为内置类型或其他包的类型添加方法
假设我们想为内置的int
类型添加一个新方法。尝试这样做会遇到编译错误,因为int
是一个内置类型,不在用户自定义的包中。
Go
func (i int) IsPositive() bool {
return i > 0
}
同样地,如果你试图为来自其他包的类型定义方法,也会遇到问题。例如,尝试为time.Time
类型(定义在time
包中)添加一个方法:
Go
package main
import (
"fmt"
"time"
)
// 尝试为time包中的Time类型添加一个新方法Display
func (t time.Time) Display() {
fmt.Println("The time is:", t)
}
func main() {
now := time.Now()
now.Display() // 这里会引发编译错误
}
上述代码尝试为time.Time
类型添加一个Display
方法。因为Time
类型定义在time
包中,而不是当前的包(即使是main
包),所以这将导致编译错误。
这些限制确保了Go语言包的封装性和独立性,防止了对已有类型的随意扩展,促进了代码的清晰性和维护性。要为其他包中的类型添加新的行为,通常的做法是在你自己的包中定义一个新的类型,这个新类型可以嵌入原有类型或者作为原有类型的一个字段存在。