Go 入门到精通-15-错误处理

目录

  • [🔵 Go 入门到精通:错误处理](#🔵 Go 入门到精通:错误处理)
    • [15.1 Go的错误处理哲学](#15.1 Go的错误处理哲学)
      • [15.1.1 核心理念](#15.1.1 核心理念)
      • [15.1.2 与其他语言的对比](#15.1.2 与其他语言的对比)
      • [15.1.3 为什么Go选择这种方式](#15.1.3 为什么Go选择这种方式)
    • [15.2 error 接口](#15.2 error 接口)
      • [15.2.1 error 的零值](#15.2.1 error 的零值)
      • [15.2.2 基本使用模式](#15.2.2 基本使用模式)
      • [15.2.3 标准模式](#15.2.3 标准模式)
    • [15.3 创建错误](#15.3 创建错误)
      • [15.3.1 errors.New](#15.3.1 errors.New)
      • [15.3.2 fmt.Errorf](#15.3.2 fmt.Errorf)
      • [15.3.3 错误创建方式对比](#15.3.3 错误创建方式对比)
    • [15.4 错误包装(Error Wrapping)](#15.4 错误包装(Error Wrapping))
      • [15.4.1 什么是错误包装](#15.4.1 什么是错误包装)
      • [15.4.2 多层包装](#15.4.2 多层包装)
      • [15.4.3 %w vs %v](#15.4.3 %w vs %v)
    • [15.5 errors.Is 解包判断](#15.5 errors.Is 解包判断)
      • [15.5.1 基本用法](#15.5.1 基本用法)
      • [15.5.2 工作原理](#15.5.2 工作原理)
      • [15.5.3 更多示例](#15.5.3 更多示例)
    • [15.6 errors.As 类型提取](#15.6 errors.As 类型提取)
    • [15.7 自定义错误类型](#15.7 自定义错误类型)
      • [15.7.1 基础自定义错误](#15.7.1 基础自定义错误)
      • [15.7.2 带分类的错误](#15.7.2 带分类的错误)
    • [15.8 Sentinel Error 模式](#15.8 Sentinel Error 模式)
      • [15.8.1 定义哨兵错误](#15.8.1 定义哨兵错误)
      • [15.8.2 使用哨兵错误](#15.8.2 使用哨兵错误)
      • [15.8.3 Sentinel Error 的优缺点](#15.8.3 Sentinel Error 的优缺点)
    • [15.9 panic 与 recover](#15.9 panic 与 recover)
      • [15.9.1 panic 是什么](#15.9.1 panic 是什么)
      • [15.9.2 何时使用 panic](#15.9.2 何时使用 panic)
      • [15.9.3 recover 是什么](#15.9.3 recover 是什么)
    • [15.10 defer 中的 recover](#15.10 defer 中的 recover)
      • [15.10.1 标准模式](#15.10.1 标准模式)
      • [15.10.2 在goroutine中recover](#15.10.2 在goroutine中recover)
      • [15.10.3 recover 的重要限制](#15.10.3 recover 的重要限制)
    • [15.11 错误处理最佳实践](#15.11 错误处理最佳实践)
      • [15.11.1 十大黄金法则](#15.11.1 十大黄金法则)
      • [15.11.2 更多法则](#15.11.2 更多法则)
      • [15.11.3 早返回 vs 深层嵌套](#15.11.3 早返回 vs 深层嵌套)
    • [15.12 综合案例:文件处理服务](#15.12 综合案例:文件处理服务)
    • [15.13 本章总结](#15.13 本章总结)

🔵 Go 入门到精通:错误处理

📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处

🧑‍💻 作者:布朗克168 | 系列:Go入门到精通 2026 · 第三阶段 · 核心进阶



15.1 Go的错误处理哲学

Go语言的错误处理方式与其他主流语言截然不同。它没有 try-catch ,也没有异常抛出机制(panic 并非异常替代品),而是将错误作为普通的返回值来处理。

15.1.1 核心理念

go 复制代码
// Go的错误处理:显式、透明
f, err := os.Open("file.txt")
if err != nil {
    // 处理错误
    return fmt.Errorf("打开文件失败: %w", err)
}
defer f.Close()

🎯 Go哲学:"错误是值(Errors are values)。" 错误不是异常,不是特殊情况------它是正常的、预期的返回值之一。

15.1.2 与其他语言的对比

特性 Go Java Python Rust
错误机制 返回值 try-catch异常 try-except异常 Result枚举
错误传递 显式逐层返回 异常冒泡 异常冒泡 ? 传播
性能开销 低(值传递) 高(栈展开) 高(栈展开) 低(枚举)
强制处理 ✅ 编译器不强制但lint可查 ❌ 不检查异常 ❌ 不检查异常 ✅ 编译器强制
可读性 显式,有时啰嗦 隐式,可能吞异常 隐式 显式+语法糖

15.1.3 为什么Go选择这种方式

复制代码
1. 显式 > 隐式:错误处理路径一目了然
2. 简单的错误处理不应比"正常"代码更复杂
3. 强制程序员面对每个可能的错误
4. 性能:异常机制在Go中代价太高(panic需要栈展开)

15.2 error 接口

Go的 error 是一个内置接口,简单得令人惊讶:

go 复制代码
// 来自 builtin 包
type error interface {
    Error() string
}

只有一个方法 Error() string 。任何实现了 Error() string 的类型都是一个错误。

15.2.1 error 的零值

go 复制代码
var err error
fmt.Println(err == nil) // true(零值是 nil)

15.2.2 基本使用模式

go 复制代码
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil  // nil 表示没有错误
}

// 调用方必须检查错误
result, err := divide(10, 0)
if err != nil {
    fmt.Println("计算出错:", err)
    return
}
fmt.Println("结果:", result)

15.2.3 标准模式

go 复制代码
// Go代码中最常见的模式
func DoSomething() error {
    result, err := step1()
    if err != nil {
        return err
    }

    err = step2(result)
    if err != nil {
        return fmt.Errorf("step2失败: %w", err)
    }

    return nil
}

15.3 创建错误

Go提供了多种创建错误的方式,适用于不同场景。

15.3.1 errors.New

go 复制代码
import "errors"

// 创建简单错误
var ErrNotFound = errors.New("记录未找到")
var ErrTimeout = errors.New("操作超时")

注意errors.New 创建的错误是不可变的,适合作为包级别的哨兵错误(sentinel error)。

15.3.2 fmt.Errorf

go 复制代码
import "fmt"

// 格式化错误信息(支持参数)
err := fmt.Errorf("用户 %s 不存在 (ID: %d)", userName, userID)

// 带 %w 包裹另一个错误(Go 1.13+)
err := fmt.Errorf("查询用户失败: %w", originalErr)

15.3.3 错误创建方式对比

方式 适用场景 示例
errors.New("msg") 简单固定错误 ErrNotFound
fmt.Errorf("fmt %s", arg) 带动态信息的错误 参数化错误信息
fmt.Errorf("... %w", err) 包裹(wrap)底层错误 添加上下文+保留原错误
自定义错误类型 需要携带额外信息 错误码、分类等

15.4 错误包装(Error Wrapping)

Go 1.13 引入的错误包装机制是错误处理的重要演进。

15.4.1 什么是错误包装

go 复制代码
// 原始错误
err := sql.ErrNoRows

// 包装错误(添加更多上下文)
wrappedErr := fmt.Errorf("查询用户失败: %w", err)

fmt.Println(wrappedErr.Error())
// "查询用户失败: sql: no rows in result set"

关键%w 动词在 fmt.Errorf 中用来包装另一个错误,保留其"身份"。

15.4.2 多层包装

go 复制代码
// 错误链:底层 → 中层 → 顶层
err1 := errors.New("数据库连接被拒绝")              // 底层
err2 := fmt.Errorf("连接主库失败: %w", err1)       // 中层
err3 := fmt.Errorf("初始化服务失败: %w", err2)      // 顶层

fmt.Println(err3.Error())
// "初始化服务失败: 连接主库失败: 数据库连接被拒绝"

错误链示意图

复制代码
初始化服务失败  →  连接主库失败  →  数据库连接被拒绝
   (err3)            (err2)           (err1)
     ↑ 外层             ↑ 中层           ↑ 底层(根因)

15.4.3 %w vs %v

动词 作用 能否用 errors.Is/errors.As 解包
%w 包装错误,保留错误链 ✅ 可以
%v 仅格式化错误信息为字符串 ❌ 不可以(断链)
go 复制代码
// ✅ 用 %w:保留错误链
wrapped := fmt.Errorf("失败: %w", originalErr)
errors.Is(wrapped, originalErr)  // true

// ❌ 用 %v:断链
broken := fmt.Errorf("失败: %v", originalErr)
errors.Is(broken, originalErr)    // false!

15.5 errors.Is 解包判断

errors.Is 用于判断错误链中是否包含某个特定错误

15.5.1 基本用法

go 复制代码
import "errors"

// 哨兵错误
var ErrNotFound = errors.New("not found")

func queryUser(id int) error {
    return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
}

func main() {
    err := queryUser(42)

    // ✅ errors.Is 沿着错误链查找
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户不存在")
    }

    // ❌ 不要直接用 == (只能判断最外层)
    if err == ErrNotFound {  // false!因为被包装了
        // 不会执行
    }
}

15.5.2 工作原理

复制代码
errors.Is(err, target) 沿着错误链逐个比对:
  1. err 自己 == target?
  2. err.Unwrap() == target?(如果 err 实现了 Unwrap())
  3. err.Unwrap().Unwrap() == target?
  4. ... 直到链结束或找到匹配

15.5.3 更多示例

go 复制代码
// 标准库的错误
_, err := os.Open("notexist.txt")
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("文件不存在,创建新文件")
}

// 对比多个错误
if errors.Is(err, io.EOF) {
    fmt.Println("读取完毕")
} else if errors.Is(err, os.ErrPermission) {
    fmt.Println("没有权限")
}

15.6 errors.As 类型提取

errors.As 用于从错误链中提取特定类型的错误

15.6.1 基本用法

go 复制代码
import "errors"

// 自定义错误类型
type ValidationError struct {
    Field string
    Value any
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Msg)
}

// 使用
func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field: "age",
            Value: age,
            Msg:   "年龄不在有效范围内",
        }
    }
    return nil
}

func process() error {
    err := validateAge(200)
    if err != nil {
        return fmt.Errorf("处理失败: %w", err)
    }
    return nil
}

func main() {
    err := process()

    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("校验失败!字段=%s, 值=%v, 原因=%s\n",
            valErr.Field, valErr.Value, valErr.Msg)
    }
}

15.6.2 errors.Is vs errors.As

维度 errors.Is errors.As
比较目标 具体值(sentinel error) 类型
参数 errors.Is(err, target) errors.As(err, &target)
用途 "是这个错误吗?" "是这个类型的错误吗?"
返回值 bool bool
提取信息 ❌ 只能判断 ✅ 可提取错误字段
示例 errors.Is(err, io.EOF) errors.As(err, &pathErr)

🔑 记忆口诀:"Is判断是不是这个错误,As提取是不是这种错误。"


15.7 自定义错误类型

当需要携带比字符串更多的错误信息时,自定义错误类型是最佳选择。

15.7.1 基础自定义错误

go 复制代码
// 带错误码的错误
type AppError struct {
    Code    int
    Message string
    Cause   error  // 底层原因
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// 实现 Unwrap 方法,支持 errors.Is / errors.As
func (e *AppError) Unwrap() error {
    return e.Cause
}

// 使用
func NewNotFoundError(msg string) *AppError {
    return &AppError{Code: 404, Message: msg}
}

15.7.2 带分类的错误

go 复制代码
type ErrorCategory int

const (
    CategoryValidation ErrorCategory = iota + 1  // 校验错误
    CategoryBusiness                               // 业务错误
    CategorySystem                                 // 系统错误
    CategoryExternal                               // 外部依赖错误
)

type CategorizedError struct {
    Category ErrorCategory
    Message  string
    Cause    error
}

func (e *CategorizedError) Error() string { return e.Message }
func (e *CategorizedError) Unwrap() error { return e.Cause }

// 根据分类做不同处理
func handleError(err error) {
    var catErr *CategorizedError
    if errors.As(err, &catErr) {
        switch catErr.Category {
        case CategoryValidation:
            fmt.Println("⚠️ 客户端输入有误")
        case CategorySystem:
            fmt.Println("🔴 系统故障,触发告警")
        case CategoryExternal:
            fmt.Println("🟡 外部服务异常,启用降级")
        }
    }
}

15.8 Sentinel Error 模式

**Sentinel Error(哨兵错误)**是预定义的、包级别的错误值,调用方可以用 errors.Is 来判断。

15.8.1 定义哨兵错误

go 复制代码
package userservice

import "errors"

// 包级别的哨兵错误
var (
    ErrNotFound      = errors.New("用户未找到")
    ErrAlreadyExists = errors.New("用户已存在")
    ErrInvalidEmail  = errors.New("邮箱格式无效")
    ErrUnauthorized  = errors.New("未授权操作")
)

15.8.2 使用哨兵错误

go 复制代码
// 服务层返回哨兵错误
func GetUser(id int) (*User, error) {
    user, err := db.QueryUser(id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrNotFound  // 转换为业务哨兵错误
    }
    if err != nil {
        return nil, fmt.Errorf("查询用户: %w", err)
    }
    return user, nil
}

// 调用方判断
user, err := GetUser(42)
if errors.Is(err, ErrNotFound) {
    // 返回404
}

15.8.3 Sentinel Error 的优缺点

优点 缺点
简单直观 调用方产生对特定包的依赖
易于判断 无法携带动态信息
生态广泛使用 暴露实现细节
errors.Is 原生支持 错误信息固定

💡 建议 :哨兵错误适用于稳定的、为数不多的错误类型。动态信息用自定义错误类型。


15.9 panic 与 recover

panicrecover 是Go的"紧急机制",不是异常处理工具

15.9.1 panic 是什么

go 复制代码
func divide(a, b int) int {
    if b == 0 {
        panic("除数不能为零!")  // 程序崩溃
    }
    return a / b
}

// 调用会导致:
// panic: 除数不能为零!
// goroutine 1 [running]:
// main.divide(...)
// main.main()

panic 会:

  1. 立即停止当前函数执行
  2. 执行所有已注册的 defer
  3. 向调用栈上方传播
  4. 最终程序退出(如果没有 recover

15.9.2 何时使用 panic

✅ 可以使用 panic ❌ 不应用 panic
程序无法继续运行的致命错误 正常的业务错误
编程错误(如数组越界) 用户输入无效
初始化失败(init函数中) 文件不存在
不变量被破坏 网络超时
go 复制代码
// ✅ panic 的合理使用
func MustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(err)  // 编译时就应该发现的错误
    }
    return re
}

// ❌ panic 的滥用
func GetUser(id int) *User {
    user, err := db.Find(id)
    if err != nil {
        panic(err)  // 不要把业务错误变成 panic!
    }
    return user
}

15.9.3 recover 是什么

go 复制代码
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
        }
    }()

    panic("出错了!")
    fmt.Println("这行不会执行")
}

func main() {
    safeCall()
    fmt.Println("程序继续运行")  // ✅ 会执行
}

recover 只能在 defer 函数中生效,它截获 panic,让程序恢复运行。


15.10 defer 中的 recover

15.10.1 标准模式

go 复制代码
// HTTP服务器中防止单个请求panic导致整个服务崩溃
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("PANIC: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

15.10.2 在goroutine中recover

go 复制代码
// ⚠️ 每个goroutine必须有自己的recover!
go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("goroutine panic:", r)
        }
    }()

    // 可能panic的代码
    riskyOperation()
}()

// ❌ 外层recover无法捕获goroutine中的panic
go func() {
    riskyOperation()  // panic会导致整个程序崩溃!
}()

15.10.3 recover 的重要限制

go 复制代码
// ❌ recover 不能在 defer 外面生效
func wrong() {
    r := recover()  // 永远返回 nil!
    fmt.Println(r)
}

// ❌ recover 不能跨goroutine
// ❌ recover 不能捕获 os.Exit 导致的退出
// ❌ recover 不能捕获 fatal error(如栈溢出)

15.11 错误处理最佳实践

15.11.1 十大黄金法则

1. 永远不要忽略错误

go 复制代码
// ❌ 忽略错误
json.Unmarshal(data, &v)

// ✅ 处理错误
if err := json.Unmarshal(data, &v); err != nil {
    return fmt.Errorf("解析JSON失败: %w", err)
}

2. 错误只处理一次

go 复制代码
// ❌ 打印后又返回------重复处理
if err != nil {
    log.Println("错误:", err)  // 处理了
    return err                 // 又传递了
}

// ✅ 要么处理(吞掉),要么传递(返回),二选一
if err != nil {
    return fmt.Errorf("操作失败: %w", err)  // 添加上下文后传递
}

3. 添加上下文信息

go 复制代码
// ❌ 仅返回底层错误
return err

// ✅ 添加有意义的上下文
return fmt.Errorf("用户 %d 查询订单失败: %w", userID, err)

4. 使用 %w 保持错误链

go 复制代码
return fmt.Errorf("读取配置: %w", err)   // ✅ 保留
return fmt.Errorf("读取配置: %v", err)   // ❌ 断链

5. 错误信息小写开头、无标点

go 复制代码
// ✅ Go惯例:小写开头,无句号
errors.New("用户未找到")
fmt.Errorf("id=%d: %w", id, err)

// ❌ 违反惯例
errors.New("User not found.")
errors.New("错误:用户未找到!")

15.11.2 更多法则

# 法则 说明
6 尽早返回错误 减少嵌套 if err == nil { ... }
7 定义哨兵错误 可预期的固定错误用 var ErrXxx
8 错误判断用 errors.Is/As 不用 == 判断包装后的错误
9 panic只用于不可恢复的错误 业务错误永远是 error 返回值
10 每个goroutine都要recover 一个goroutine的panic不会自动被另一个捕获

15.11.3 早返回 vs 深层嵌套

go 复制代码
// ❌ 深层嵌套
func process() error {
    f, err := os.Open("file.txt")
    if err == nil {
        defer f.Close()
        data, err := io.ReadAll(f)
        if err == nil {
            result, err := parse(data)
            if err == nil {
                return save(result)
            } else {
                return err
            }
        } else {
            return err
        }
    } else {
        return err
    }
}

// ✅ 早返回(Happy Path左对齐)
func process() error {
    f, err := os.Open("file.txt")
    if err != nil {
        return fmt.Errorf("打开文件失败: %w", err)
    }
    defer f.Close()

    data, err := io.ReadAll(f)
    if err != nil {
        return fmt.Errorf("读取文件失败: %w", err)
    }

    result, err := parse(data)
    if err != nil {
        return fmt.Errorf("解析失败: %w", err)
    }

    return save(result)
}

15.12 综合案例:文件处理服务

go 复制代码
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "os"
)

// ── 哨兵错误 ──
var (
    ErrFileNotFound = errors.New("文件未找到")
    ErrInvalidJSON  = errors.New("无效的JSON格式")
    ErrEmptyContent = errors.New("内容为空")
)

// ── 自定义错误类型 ──
type FileError struct {
    Op   string // 操作:open/read/parse
    Path string // 文件路径
    Err  error  // 底层错误
}

func (e *FileError) Error() string {
    return fmt.Sprintf("文件操作[%s] %s 失败: %v", e.Op, e.Path, e.Err)
}

func (e *FileError) Unwrap() error { return e.Err }

// ── 服务层 ──
type Config struct {
    Port    int    `json:"port"`
    Host    string `json:"host"`
    Debug   bool   `json:"debug"`
}

func LoadConfig(path string) (*Config, error) {
    // 1. 打开文件
    data, err := os.ReadFile(path)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return nil, fmt.Errorf("%w: %s", ErrFileNotFound, path)
        }
        return nil, &FileError{Op: "read", Path: path, Err: err}
    }

    // 2. 验证非空
    if len(data) == 0 {
        return nil, ErrEmptyContent
    }

    // 3. 解析JSON
    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err)
    }

    // 4. 业务校验
    if config.Port <= 0 || config.Port > 65535 {
        return nil, fmt.Errorf("端口号无效: %d", config.Port)
    }

    return &config, nil
}

// ── 调用方 ──
func main() {
    config, err := LoadConfig("config.json")
    if err != nil {
        // 分类处理
        switch {
        case errors.Is(err, ErrFileNotFound):
            fmt.Println("📁 配置文件不存在,使用默认配置")
            // 使用默认配置

        case errors.Is(err, ErrInvalidJSON):
            fmt.Println("📝 配置文件格式错误:", err)
            return

        case errors.Is(err, ErrEmptyContent):
            fmt.Println("📄 配置文件为空")
            return

        default:
            // 尝试提取文件错误详情
            var fileErr *FileError
            if errors.As(err, &fileErr) {
                fmt.Printf("🔴 文件操作异常 [%s]: %s\n", fileErr.Op, fileErr.Path)
            }
            fmt.Println("❌ 加载配置失败:", err)
            return
        }
    }

    fmt.Printf("✅ 配置加载成功: %+v\n", config)
}

15.13 本章总结

知识点 要点回顾
Go错误哲学 错误是值,显式处理,不依赖异常
error接口 Error() string 一个方法
创建错误 errors.Newfmt.Errorf(用 %w 包装)
错误包装 %w 保留错误链;%v 断链
errors.Is 沿链判断是否包含特定值(sentinel)
errors.As 沿链提取特定类型的错误
自定义错误 实现 Error() + Unwrap()
Sentinel Error 包级 var ErrXxx = errors.New(...)
panic/recover 紧急机制,不是异常处理;defer中recover
最佳实践 不忽略、不重复处理、添加上下文、早返回

💬 思考与互动 :你在项目中是如何组织错误处理的?用了自定义错误类型还是哨兵错误?遇到过 errors.Iserrors.As 的混淆吗?欢迎评论区分享你的Go错误处理心法!


本文是「Go入门到精通 2026」系列第15篇。上一篇:14-Go 入门到精通-包与模块管理 | 下一篇:敬请期待...

相关推荐
m0_547486662 小时前
《Python程序设计与数据分析》全套PPT课件
python·数据分析
zzb15802 小时前
Zed 配置 Swift / iOS 开发
开发语言·ios·swift
盼小辉丶2 小时前
OpenCV-Python实战(33)——构建实时目标检测与跟踪系统
python·opencv·目标检测·计算机视觉
bill32822780433 小时前
从 0 到 1 掌握 1688API:商品详情获取技巧
python
北冥you鱼4 小时前
Go-Ethereum (Geth) 最佳实践:从部署到生产环境优化
开发语言·后端·golang
这不小天嘛10 小时前
JAVA八股——J集合篇
java·开发语言
hboot10 小时前
AI工程师第五课 - 大语言模型基础
python·llm·fastapi
AOwhisky11 小时前
Python 学习笔记(第一期与第二期)——基础语法——核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发
STLearner11 小时前
ICML 2026 | LLM×Graph论文总结[1]【图基础模型,文本属性图,多模态属性图,图对齐,图提示学习,关系深度学习
论文阅读·人工智能·python·深度学习·学习·机器学习·数据挖掘