自定义类型
Go 提供了强大的类型系统,允许开发者基于已有类型定义新的类型。通过 type 关键字,可以创建具有独立语义的类型,为类型添加方法,实现接口,从而构建出类型安全、表达力强的代码。
type 定义新类型
使用 type NewType OriginalType 可以基于已有类型定义一个全新的类型:
// 基于内置类型定义新类型
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
type Kilometer float64 // 公里
type ID int64 // 用户 ID
type UserID int64 // 用户 ID(语义更明确)
// 基于复合类型定义新类型
type StringList []string
type IntMap map[string]int
type Point struct {
X, Y float64
}type NewType OriginalType 创建了一个与原始类型不同的新类型。新类型与原始类型拥有相同的底层表示,但它们是两个不同的类型,不能直接互相赋值。新类型可以拥有自己的方法集。
type Celsius float64
type Fahrenheit float64
var c Celsius = 100.0
var f Fahrenheit = 212.0
// ❌ 编译错误:不能将 Celsius 赋值给 Fahrenheit
// f = c // cannot use c (type Celsius) as type Fahrenheit in assignment
// ✅ 需要显式类型转换
f = Fahrenheit(c) // 类型转换
c = Celsius(f)
fmt.Println(c) // 100 —— 实际上是 Celsius 类型的 100.0
fmt.Printf("c 的类型: %T\n", c) // c 的类型: main.Celsius
fmt.Printf("f 的类型: %T\n", f) // f 的类型: main.Fahrenheit类型别名 type Alias = Original
Go 1.9 引入了类型别名(Type Alias),使用 = 符号:
type Alias = OriginalType
// 示例
type MyString = string // MyString 是 string 的别名
type Rune = int32 // rune 的实际定义(标准库中就是用类型别名定义的)
type Byte = uint8 // byte 的实际定义type Alias = OriginalType 创建了一个原始类型的别名。别名与原始类型是完全相同的类型,可以互相赋值,方法集也完全相同。别名只是一个”名字”,不是新类型。
type MyString = string
var s string = "hello"
var ms MyString = "world"
// ✅ 类型别名可以互相赋值(因为它们是同一个类型)
s = ms // 直接赋值,无需转换
ms = s // 直接赋值,无需转换
fmt.Println(s) // world
fmt.Println(ms) // world
// MyString 可以使用所有 string 的方法
fmt.Println(len(ms)) // 5
fmt.Println(strings.ToUpper(ms)) // WORLD类型定义与类型别名的区别
这是 Go 面试和日常开发中最容易混淆的知识点之一:
| 特性 | 类型定义 type New Original | 类型别名 type Alias = Original |
|---|---|---|
| 是否产生新类型 | ✅ 是,全新的类型 | ❌ 否,只是别名 |
| 能否直接互相赋值 | ❌ 需要显式转换 | ✅ 可以直接赋值 |
| 能否共享方法 | ❌ 不共享,各有自己的方法集 | ✅ 完全共享 |
reflect.TypeOf 结果 | main.NewType | Original(原始类型名) |
| 底层数据是否相同 | 相同的内存表示 | 完全相同 |
| 适用场景 | 需要类型安全、独立方法 | 代码迁移、简化长类型名 |
package main
import (
"fmt"
"reflect"
)
type MyInt int // 类型定义
type YourInt = int // 类型别名
func main() {
var a MyInt = 10
var b YourInt = 20
var c int = 30
// 类型定义:不能直接赋值
// a = c // 编译错误
a = MyInt(c) // 需要显式转换
// 类型别名:可以直接赋值
b = c // ✅ 直接赋值
c = b // ✅ 直接赋值
b = int(c) // ✅ 也可以转换(但不是必须的)
fmt.Printf("a 的类型: %T\n", a) // a 的类型: main.MyInt
fmt.Printf("b 的类型: %T\n", b) // b 的类型: int
fmt.Printf("c 的类型: %T\n", c) // c 的类型: int
fmt.Println(reflect.TypeOf(a)) // main.MyInt
fmt.Println(reflect.TypeOf(b)) // int
}💡 标准库中的实际应用:
type byte = uint8——byte是uint8的别名type rune = int32——rune是int32的别名type error interface{ ... }——error是一个接口类型定义
何时使用类型定义
当你需要类型安全和独立的语义时,使用类型定义:
// ✅ 类型定义的使用场景
// 场景一:防止混淆相同底层类型的不同概念
type UserID int64 // 用户 ID
type OrderID int64 // 订单 ID
func getUser(id UserID) { /* ... */ }
func getOrder(id OrderID) { /* ... */ }
var uid UserID = 1001
var oid OrderID = 5001
getUser(uid) // ✅ 类型正确
// getUser(oid) // ❌ 编译错误:不能将 OrderID 作为 UserID 使用
// 场景二:为新类型添加方法
type Duration int64 // 纳秒
func (d Duration) Hours() float64 {
return float64(d) / float64(1e9) / 3600
}
func (d Duration) String() string {
return fmt.Sprintf("%.2fh", d.Hours())
}
d := Duration(3600 * 1e9) // 1 小时
fmt.Println(d.String()) // 1.00h何时使用类型别名
// ✅ 类型别名的使用场景
// 场景一:大规模代码重构,逐步迁移类型名
// 旧代码中:type OldName struct { ... }
// 重构时:type OldName = NewName
// 这样旧代码仍然可以编译,同时逐步迁移到 NewName
// 场景二:简化复杂的类型表达式
type HandlerFunc = func(http.ResponseWriter, *http.Request)
type StringMap = map[string]string
type IntSlice = []int方法定义
方法(Method)是带有接收者(Receiver)的函数。只有自定义类型(类型定义)才能添加方法,内置类型和类型别名(除非别名的是同一包中已定义的类型)不能直接添加方法。
值接收者 vs 指针接收者
type Rect struct {
Width float64
Height float64
}
// 值接收者:接收的是 Rect 的副本
func (r Rect) Area() float64 {
return r.Width * r.Height
}
// 指针接收者:接收的是 *Rect,可以修改原始值
func (r *Rect) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
r := Rect{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2.0)
fmt.Printf("%+v\n", r) // {Width:20 Height:10}
}两者对比
| 特性 | 值接收者 func (r T) | 指针接收者 func (r *T) |
|---|---|---|
| 接收的是什么 | 结构体的副本(值拷贝) | 结构体的指针 |
| 能否修改原始值 | ❌ 不能 | ✅ 可以 |
| 调用方式 | v.Method() 或 (&v).Method() | p.Method() 或 (*p).Method() |
| 内存开销 | 拷贝整个值(大结构体开销大) | 只拷贝指针(8 字节) |
| 是否避免 nil panic | 不会(值不存在 nil) | 可能(如果指针为 nil) |
选择规则
- 默认使用值接收者:如果方法只是读取数据,不修改接收者
- 必须使用指针接收者:如果方法需要修改接收者
- 建议使用指针接收者:如果结构体较大(避免拷贝开销)
- 一致性:如果一个类型的某个方法使用了指针接收者,建议所有方法都使用指针接收者
- 值接收者的优点:确保方法不会意外修改接收者,适合不可变语义
type Counter struct {
count int
}
// 修改状态 → 指针接收者
func (c *Counter) Increment() {
c.count++
}
// 读取状态 → 值接收者也可以,但为了一致性,建议用指针接收者
func (c *Counter) Value() int {
return c.count
}
// 如果结构体很大,即使不修改也用指针接收者以避免拷贝
type BigStruct struct {
data [1024]byte
// ... 很多字段
}
func (b *BigStruct) Process() {
// 即使不修改 b,也用指针接收者避免拷贝 1024 字节
}💡 Go 自动取地址和解引用:Go 允许在值上调用指针接收者方法(自动取地址 &v),也允许在指针上调用值接收者方法(自动解引用 *p)。编译器会自动处理这些转换。
type Person struct {
Name string
}
func (p *Person) SetName(name string) {
p.Name = name
}
func (p Person) GetName() string {
return p.Name
}
var p = Person{Name: "Alice"}
p.SetName("Bob") // 编译器自动转换为 (&p).SetName("Bob")
fmt.Println(p.GetName()) // 编译器自动转换为 (*&p).GetName()
pp := &Person{Name: "Charlie"}
fmt.Println(pp.GetName()) // 编译器自动转换为 (*pp).GetName()方法集规则
方法集是指一个类型上所有可调用方法的集合。Go 的方法集规则决定了接口实现的判定以及方法的可见性:
- 值类型 T 的方法集包含所有值接收者的方法
- *指针类型 T 的方法集包含所有值接收者和指针接收者的方法
type Animal struct {
Name string
}
func (a Animal) Speak() string { // 值接收者
return "..."
}
func (a *Animal) SetName(name string) { // 指针接收者
a.Name = name
}
// 值类型 Animal 的方法集:{ Speak }
// 指针类型 *Animal 的方法集:{ Speak, SetName }方法集规则对接口实现有重要影响:
type Speaker interface {
Speak() string
}
type Namer interface {
SetName(name string)
GetName() string
}
// ✅ Animal 实现了 Speaker(Speak 是值接收者,值和指针都可以)
// ✅ *Animal 实现了 Speaker
// ❌ Animal 没有实现 Namer(SetName 是指针接收者,值类型不包含此方法)
// ✅ *Animal 实现了 Namer
var s Speaker = Animal{} // ✅ Animal 实现了 Speaker
var s2 Speaker = &Animal{} // ✅ *Animal 也实现了 Speaker
// var n Namer = Animal{} // ❌ 编译错误:Animal 没有 SetName 方法
var n Namer = &Animal{} // ✅ *Animal 有 SetName 方法⚠️ 常见错误:如果一个结构体的某些方法是指针接收者,那么用值类型去赋值给接口时会编译失败。此时必须使用指针。
type Writer interface {
Write([]byte) (int, error)
}
type MyWriter struct{}
// 注意:指针接收者
func (w *MyWriter) Write(p []byte) (int, error) {
return len(p), nil
}
// ❌ 编译错误
// var w Writer = MyWriter{}
// ✅ 正确:必须使用指针
var w Writer = &MyWriter{}为不同基础类型定义方法
除了结构体,你还可以基于其他类型定义方法:
// 为自定义的 slice 类型定义方法
type StringSlice []string
func (ss StringSlice) Join(sep string) string {
result := ""
for i, s := range ss {
if i > 0 {
result += sep
}
result += s
}
return result
}
func (ss *StringSlice) Add(s string) {
*ss = append(*ss, s)
}
ss := StringSlice{"Go", "is", "awesome"}
fmt.Println(ss.Join(" ")) // Go is awesome
ss.Add("!")
fmt.Println(ss.Join(" ")) // Go is awesome !// 为自定义的函数类型定义方法
type Validator func(string) bool
func (v Validator) And(other Validator) Validator {
return func(s string) bool {
return v(s) && other(s)
}
}
func (v Validator) Or(other Validator) Validator {
return func(s string) bool {
return v(s) || other(s)
}
}
func main() {
isNotEmpty := Validator(func(s string) bool { return len(s) > 0 })
hasPrefix := Validator(func(s string) bool { return len(s) > 0 && s[0] == 'A' })
combined := isNotEmpty.And(hasPrefix)
fmt.Println(combined("Apple")) // true
fmt.Println(combined("Banana")) // false
}类型断言
类型断言用于从接口类型中提取底层的具体值。语法为 x.(T),其中 x 是接口类型,T 是要断言的具体类型。如果断言失败,会触发 panic。安全的写法是 value, ok := x.(T)。
基本类型断言
var i interface{} = "hello"
// 非安全断言(失败会 panic)
s := i.(string)
fmt.Println(s) // hello
// 安全断言(使用 comma ok)
s, ok := i.(string)
fmt.Println(s, ok) // hello true
s, ok = i.(int)
fmt.Println(s, ok) // 0 false —— 断言失败,ok 为 false
// ❌ 非安全断言失败 → panic
// n := i.(int) // panic: interface conversion: interface {} is string, not int结合 switch 的类型断言
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("整数: %d\n", v)
case string:
fmt.Printf("字符串: %s\n", v)
case bool:
fmt.Printf("布尔值: %t\n", v)
case []int:
fmt.Printf("整数切片: %v\n", v)
default:
fmt.Printf("未知类型: %T\n", v)
}
}
describe(42) // 整数: 42
describe("hello") // 字符串: hello
describe(true) // 布尔值: true
describe([]int{1, 2, 3}) // 整数切片: [1 2 3]
describe(3.14) // 未知类型: float64在实际项目中的应用
// JSON 反序列化后的类型断言
import "encoding/json"
func processValue(raw json.RawMessage) {
var data interface{}
json.Unmarshal(raw, &data)
switch v := data.(type) {
case float64:
fmt.Printf("数字: %f\n", v)
case string:
fmt.Printf("字符串: %s\n", v)
case map[string]interface{}:
fmt.Printf("对象: %v\n", v)
case []interface{}:
fmt.Printf("数组: %v\n", v)
case nil:
fmt.Println("null")
}
}练习题
练习 1:自定义类型与方法
定义一个 MyFloat64 类型(基于 float64),为其添加以下方法:
Abs()返回绝对值Round()返回四舍五入后的整数Add(other MyFloat64)返回两个值之和
package main
import (
"fmt"
"math"
)
type MyFloat64 float64
func (f MyFloat64) Abs() MyFloat64 {
if f < 0 {
return -f
}
return f
}
func (f MyFloat64) Round() int64 {
return int64(math.Round(float64(f)))
}
func (f MyFloat64) Add(other MyFloat64) MyFloat64 {
return f + other
}
func main() {
a := MyFloat64(-3.7)
b := MyFloat64(2.3)
fmt.Println(a.Abs()) // 3.7
fmt.Println(a.Round()) // -4
fmt.Println(a.Add(b)) // -1.4
fmt.Println(a.Add(b).Abs()) // 1.4
}练习 2:类型定义与类型别名的区别
编写一个程序,分别使用类型定义和类型别名基于 int 创建新类型。验证:
- 类型定义的新类型能否直接赋值给
int - 类型别名能否直接赋值给
int - 为类型定义的新类型添加方法
package main
import "fmt"
// 类型定义
type Score int
// 类型别名
type AliasScore = int
// 为类型定义添加方法(不能为类型别名添加方法,因为别名不是新类型)
func (s Score) String() string {
return fmt.Sprintf("%d分", int(s))
}
func main() {
var x int = 100
var s Score = 95
var a AliasScore = 80
// 类型定义不能直接赋值
// s = x // 编译错误
s = Score(x) // 需要显式转换
fmt.Println(s.String()) // 100分
// 类型别名可以直接赋值
a = x // ✅ 直接赋值
fmt.Println(a) // 100
// AliasScore 没有自己的方法集(与 int 完全相同)
// a.String() // 编译错误:int 类型没有 String 方法
}练习 3:方法集与接口
定义一个 Shape 接口,包含 Area() 和 Scale(factor float64) 两个方法。然后定义 Rectangle 和 Circle 两个结构体,实现 Shape 接口。验证值类型和指针类型对接口实现的影响。
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Scale(factor float64)
}
type Rectangle struct {
Width, Height float64
}
// Area 是值接收者
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Scale 是指针接收者(需要修改)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func printShape(s Shape) {
fmt.Printf("面积: %.2f\n", s.Area())
}
func main() {
r := Rectangle{Width: 10, Height: 5}
c := Circle{Radius: 3}
// 指针类型实现了 Shape 接口(因为包含指针接收者的方法)
printShape(&r) // 面积: 50.00
printShape(&c) // 面积: 28.27
// ❌ 值类型不能实现 Shape 接口(Scale 是指针接收者)
// printShape(r) // 编译错误:Rectangle does not implement Shape (Scale method has pointer receiver)
// printShape(c) // 编译错误:Circle does not implement Shape (Scale method has pointer receiver)
// 通过指针接收者方法修改
r.Scale(2.0)
fmt.Printf("缩放后: %.2f\n", r.Area()) // 缩放后: 200.00
}