导航菜单

自定义类型

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 Definition)

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)

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.NewTypeOriginal(原始类型名)
底层数据是否相同相同的内存表示完全相同
适用场景需要类型安全、独立方法代码迁移、简化长类型名
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 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)

选择规则

接收者选择规则
  1. 默认使用值接收者:如果方法只是读取数据,不修改接收者
  2. 必须使用指针接收者:如果方法需要修改接收者
  3. 建议使用指针接收者:如果结构体较大(避免拷贝开销)
  4. 一致性:如果一个类型的某个方法使用了指针接收者,建议所有方法都使用指针接收者
  5. 值接收者的优点:确保方法不会意外修改接收者,适合不可变语义
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 字节
}
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()

方法集规则

方法集(Method Set)

方法集是指一个类型上所有可调用方法的集合。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
}

类型断言

类型断言(Type Assertion)

类型断言用于从接口类型中提取底层的具体值。语法为 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 创建新类型。验证:

  1. 类型定义的新类型能否直接赋值给 int
  2. 类型别名能否直接赋值给 int
  3. 为类型定义的新类型添加方法
参考答案
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) 两个方法。然后定义 RectangleCircle 两个结构体,实现 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
}

搜索