当前位置: 首页 > news >正文

go go go 出发咯 - go web开发入门系列(四) 数据库ORM框架集成与解读

go go go 出发咯 - go web开发入门系列(四) 数据库ORM框架集成与解读


往期回顾

  • go go go 出发咯 - go web开发入门系列(一) helloworld
  • go go go 出发咯 - go web开发入门系列(二) Gin 框架实战指南
  • go go go 出发咯 - go web开发入门系列(三) 项目基础框架搭建与解读

前言

在上一篇文章中,我们从零开始,搭建了一个生产级的 Go Web 应用框架。我们深入探讨了分层架构、依赖注入和面向接口编程,并最终构建了一个结构清晰、职责分明的“手动挡”应用——我们拥有对每一行 SQL 的完全控制权。

这种控制力在需要极致性能优化的场景下非常宝贵。但对于大多数标准的增删改查(CRUD)操作来说,手动编写和映射每一条 SQL 显得有些繁琐。这正是 ORM(对象关系映射)框架大显身手的舞台。

本文将作为上一篇的进阶,向您展示如何将 Go 生态中最流行的 ORM 框架 GORM,无缝地集成到我们现有的分层架构中。我们的目标是:在不改动任何 Service 和 Handler 层代码的前提下,用 GORM 完全替换掉手写 SQL 的 Repository 层,体验开发效率的飞跃。

架构回顾:解耦是替换的基石

让我们再次回顾一下我们的分层架构,正是这个清晰的结构,使得替换数据访问层成为可能。

/awesomeProject
| ├── cmd/ # 入口文件 
│ 	└── server/ 
│ 		└── main.go # 主程序入口 
├── configs/ # 配置文件 
│ 	└── config.dev.yaml 
├── internal/ # 内部模块 
│ ├── config/ # 配置加载 
│ ├── database/ # 数据库连接 
│ ├── models/ # 数据模型 
│ ├── repository/ # 数据访问层 
│ └── service/ # 业务逻辑层 
├── transport/ # 传输层 
...

GORM (全功能 ORM 框架)

GORM 介绍

GORM 是 Go 语言中最流行的全功能 ORM (Object-Relational Mapping) 框架。它的设计哲学最接近您熟悉的 HibernateMyBatis-Plus,旨在通过“约定优于配置”和链式调用,将开发者从手写 SQL 中解放出来。

核心理念: 将数据库操作完全对象化。你操作的是 Go 的结构体对象,GORM 负责在背后生成并执行对应的 SQL 语句。

特点:

链式 API: 提供非常流畅的链式调用方法 (db.Where(…).First(…))。

自动化: 自动处理创建、查询、更新、删除 (CRUD) 操作。

高级功能: 支持自动迁移(根据结构体创建/修改表)、钩子(在创建/更新前后执行特定函数)、预加载(Eager Loading)、事务等。

优点:

  • 开发效率极高,尤其适合快速构建原型和标准的 CRUD 应用。

  • 代码量显著减少,可读性强(对于熟悉 ORM 的人而言)。

缺点:“魔法”太多,可能会隐藏底层 SQL 的性能问题。

  • 对于复杂的查询,其链式 API 可能变得复杂,或者不得不退回手写 SQL。

  • 学习曲线相对较陡,需要理解其内部的约定和工作方式。

GORM 代码集成示例

本节我们将继续在次框架上,进行实现商品(product)相关的CRUD操作,并给与外部调用,对于商品(product)整个链路过程将采用ORM的方式,便于和之前实现的用户(User)相对比学习。

第1步:安装 GORM 及驱动

GORM 的工作需要两个核心组件:GORM 核心库和对应数据库的驱动。

# 安装 GORM 核心库
go get -u gorm.io/gorm# 安装 GORM 的 MySQL 驱动适配器
go get -u gorm.io/driver/mysql

还记得在上一节中我们连接 mysql 数据库时引入的依赖 go get -u github.com/go-sql-driver/mysql 吗?

Q:" go get -u github.com/go-sql-driver/mysql ; go get -u gorm.io/gorm ; go get -u gorm.io/driver/mysql " 这三个依赖不冲突吗?

A:

gorm.io/gorm (ORM 框架本身)

  • 这是 GORM 的核心库,提供了所有 .Create(), .First(), .Where() 等链式调用方法。
  • 它是一个高层抽象,负责将对象操作转换为 SQL 思想。但它自己并不知道如何与具体的数据库(如 MySQL 或 PostgreSQL)对话。

gorm.io/driver/mysql (GORM 的 MySQL 适配器)

  • 这个库是连接 GORM 核心框架和底层数据库驱动的“桥梁”或“适配器”。
  • 它告诉 GORM:“当你需要操作 MySQL 时,应该使用这种方式来配置和传递指令。”

github.com/go-sql-driver/mysql (底层的数据库驱动)

  • 这是真正负责与 MySQL 服务器进行网络通信、执行 SQL 语句的“工人”。
  • gorm.io/driver/mysql 这个“适配器”在内部会依赖并调用这个底层的驱动来完成实际工作。

gorm.io/driver/mysql 在底层依赖了 go-sql-driver/mysql,Go 的模块工具会自动处理这个依赖关系。

第2步:创建领域模型 (Model)

我们在 internal/models 下创建产品(product)结构体,对比 SpringBoot 作为数据库实体映射

GORM 可以通过嵌入 gorm.Model 来为我们的结构体自动添加 ID, CreatedAt, UpdatedAt, DeletedAt 等常用字段。

// internal/models/product.go
package modelsimport "gorm.io/gorm"type product struct {gorm.Model         // 嵌入gorm.Model,自动获得ID和时间戳字段Name       string  `gorm:"size:255;not null"` // 使用 GORM 标签定义列属性Price      float64 `gorm:"type:decimal(10,2)"`Stock      int     `gorm:"default:0"`
}//上述结构体等价于//type product struct {
//  ID        uint           `gorm:"primaryKey"`
//  CreatedAt time.Time
//  UpdatedAt time.Time
//  DeletedAt gorm.DeletedAt `gorm:"index"`
//  Name       string  `gorm:"size:255;not null"` // 使用 GORM 标签定义列属性
//  Price      float64 `gorm:"type:decimal(10,2)"`
//  Stock      int     `gorm:"default:0"`
//}
//
第3步:创建 GORM 数据库连接

internal/database/ 下创建 gorm.go 来初始化 GORM 的数据库连接。

//internal/database/gorm.go
package databaseimport ("awesomeProject/internal/config""gorm.io/driver/mysql""gorm.io/gorm"
)// NewGormConnection 负责根据配置创建GORM数据库连接池
func NewGormConnection(dbConfig config.DatabaseConfig) (*gorm.DB, error) {// dsn 来自于我们的配置文件dsn := dbConfig.DSN// 使用GORM的MySQL驱动来打开数据库连接db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {return nil, err}// 获取底层的 *sql.DB 对象来设置连接池参数sqlDB, err := db.DB()if err != nil {return nil, err}// 设置从配置中读取的连接池参数sqlDB.SetMaxIdleConns(dbConfig.MaxIdleConns)sqlDB.SetMaxOpenConns(dbConfig.MaxOpenConns)// 可以选择在这里 Ping 数据库以验证连接if err = sqlDB.Ping(); err != nil {return nil, err}return db, nil
}
第4步:实现关于"产品"的 GORM Repository

internal/repository/ 下创建 ProductRepository.go 使用GORM 来操作数据库,生成对于product的CURD 方法

//internal/repository/ProductRepository.go
package repositoryimport ("awesomeProject/internal/models""context""gorm.io/gorm"
)// ProductRepository 定义了产品数据的所有操作,便于解耦
type ProductRepository interface {Create(ctx context.Context, product *models.Product) errorFindByID(ctx context.Context, id int64) (*models.Product, error)FindAll(ctx context.Context) ([]*models.Product, error)Update(ctx context.Context, product *models.Product) errorDelete(ctx context.Context, id int64) error
}// 构造方法
type gormMySqlProductRepository struct {db *gorm.DB // 应用的是grom 框架 这里持有的是 *gorm.DB 而不是 *sql.DB
}func (g gormMySqlProductRepository) Create(ctx context.Context, product *models.Product) error {result := g.db.WithContext(ctx).Create(product)return result.Error
}func (g gormMySqlProductRepository) FindByID(ctx context.Context, id int64) (*models.Product, error) {var product models.Productresult := g.db.WithContext(ctx).First(&product, id)if result.Error != nil {if result.Error == gorm.ErrRecordNotFound {return nil, nil}return nil, result.Error}return &product, nil}func (g gormMySqlProductRepository) FindAll(ctx context.Context) ([]*models.Product, error) {var products []*models.Productresult := g.db.WithContext(ctx).Find(&products)if result.Error != nil {if result.Error == gorm.ErrRecordNotFound {return nil, nil}return nil, result.Error}return products, nil
}func (g gormMySqlProductRepository) Update(ctx context.Context, product *models.Product) error {result := g.db.WithContext(ctx).Save(product)if result.Error != nil {return result.Error}return nil
}func (g gormMySqlProductRepository) Delete(ctx context.Context, id int64) error {result := g.db.WithContext(ctx).Delete(&models.Product{}, id)return result.Error
}// NewProductRepository 创建一个新的 ProductRepository 实例
func NewProductRepository(db *gorm.DB) ProductRepository {return &gormMySqlProductRepository{db: db}
}

Q: "gorm 官方文档中显示可以使用db.create直接操作数据库,比如: 新增数据直接使用db.create(bean),但是上述代码中使用的是 g.db.WithContext(ctx).create 这是为什么 "?

A:虽然直接使用 r.db.Create(product) 在功能上可以成功插入数据,但使用 r.db.WithContext(ctx).Create(product) 是一种更专业、更具弹性的最佳实践

详细解释一下 WithContext(ctx) 带来的三大好处:

1. 请求取消传播 (Cancellation Propagation)

  • 场景: 一个用户向您的服务器发送了创建产品的请求,但这个请求需要执行一个耗时很长的数据库操作。在操作完成前,用户不耐烦地关闭了浏览器,或者网络中断了。
  • 不使用 WithContext 的情况: 您的服务器对此一无所知。即使请求的另一端已经没人等待了,数据库操作依然会继续执行,直到完成。这白白浪费了宝贵的数据库连接和服务器资源。
  • 使用 WithContext 的情况: Gin 会为每个 HTTP 请求创建一个 context (ctx)。当用户断开连接时,Gin 会“取消”这个 ctxWithContext(ctx) 会将这个“取消”信号传递给 GORM,GORM 再传递给底层的数据库驱动。驱动程序收到信号后,可以提前终止那个正在执行的、已经没有意义的数据库查询,从而立即释放资源。

2. 超时控制 (Timeout Control)

  • 场景: 您可以为整个请求或某个特定的操作设置一个超时时间。比如,您规定任何数据库操作都不能超过3秒。
  • 不使用 WithContext 的情况: 如果某个数据库查询因为锁或者性能问题卡住了,它可能会永远地挂起,永久性地占用一个数据库连接,直到数据库自己超时。
  • 使用 WithContext 的情况: 您可以在 Service 层或 Handler 层创建一个带超时的 context (e.g., context.WithTimeout(ctx, 3*time.Second))。如果数据库操作在3秒内没有完成,ctx 会自动被取消。WithContext 感知到这个取消信号后,会立即终止数据库操作,并返回一个超时错误。这可以有效地防止慢查询拖垮整个系统。

3. 传递元数据 (Passing Metadata)

  • 场景: 在复杂的微服务架构中,您需要追踪一个请求经过了哪些服务。通常会有一个全局唯一的“追踪ID (Trace ID)”。
  • context 的作用: context 是在函数调用链中安全地传递这类请求范围内的元数据(如 Trace ID)的标准方式,而无需修改每个函数的参数列表。GORM 和很多其他库都能与 OpenTelemetry 等链路追踪系统集成,从 ctx 中提取这些信息用于日志和监控。
第5步:实现关于"产品"的 productService

之前在实现 userservice 时,对于 userservice 我们没有做到抽象成接口的形式,直接将userservice 做结构体进行声明,在此我会将productservice进行抽象,抽象成一个接口的形式。

package serviceimport ("awesomeProject/internal/models"     "awesomeProject/internal/repository" "context""errors" // 导入errors包,用于创建自定义错误
)// ProductService 定义了产品相关的业务逻辑接口
type ProductService interface {CreateProduct(ctx context.Context, name string, price float64, stock int) (*models.Product, error)GetProduct(ctx context.Context, id int64) (*models.Product, error)GetAllProducts(ctx context.Context) ([]*models.Product, error)UpdateProduct(ctx context.Context, id int64, name string, price float64, stock int) (*models.Product, error)DeleteProduct(ctx context.Context, id int64) error
}// productService 是 ProductService 的具体实现
type productService struct {productRepo repository.ProductRepository // 它依赖于 ProductRepository 接口,而不是具体实现
}// NewProductService 是 ProductService 的构造函数
func NewProductService(repo repository.ProductRepository) ProductService {return &productService{productRepo: repo}
}// CreateProduct 处理创建新产品的业务逻辑
func (s *productService) CreateProduct(ctx context.Context, name string, price float64, stock int) (*models.Product, error) {// 在这里可以添加业务逻辑,例如:// 1. 验证产品名称是否有效if name == "" {return nil, errors.New("product name cannot be empty")}// 2. 验证价格是否合法if price <= 0 {return nil, errors.New("product price must be positive")}// 3. 产品库存是否合法if stock < 0 {return nil, errors.New("product stock cannot be negative")}// 创建一个新的产品模型实例product := &models.Product{Name:  name,Price: price, Stock: stock,}// 调用仓库层来持久化数据err := s.productRepo.Create(ctx, product)if err != nil {return nil, err}return product, nil
}// GetProduct 处理获取单个产品的业务逻辑
func (s *productService) GetProduct(ctx context.Context, id int64) (*models.Product, error) {// 直接调用仓库层。return s.productRepo.FindByID(ctx, id)
}// GetAllProducts 处理获取所有产品的业务逻辑
func (s *productService) GetAllProducts(ctx context.Context) ([]*models.Product, error) {return s.productRepo.FindAll(ctx)
}// UpdateProduct 处理更新产品的业务逻辑
func (s *productService) UpdateProduct(ctx context.Context, id int64, name string, price float64, stock int) (*models.Product, error) {// 1. 首先,获取要更新的产品product, err := s.productRepo.FindByID(ctx, id)if err != nil {return nil, err // 如果在查找过程中发生数据库错误}if product == nil {return nil, errors.New("product not found") // 如果产品不存在}// 2. 更新产品的字段product.Name = nameproduct.Price = priceproduct.Stock = stock// 3. 在这里可以添加更复杂的验证逻辑...// 4. 调用仓库层的更新方法err = s.productRepo.Update(ctx, product)if err != nil {return nil, err}return product, nil
}// DeleteProduct 处理删除产品的业务逻辑
func (s *productService) DeleteProduct(ctx context.Context, id int64) error {// 在删除前,可以添加权限检查等业务逻辑// 例如:检查当前用户是否有权限删除该产品return s.productRepo.Delete(ctx, id)
}
第6步:实现全新的productHandler.go
package httpimport ("awesomeProject/internal/service" "github.com/gin-gonic/gin""net/http""strconv"
)// ProductHandler 负责处理产品相关的HTTP请求
type ProductHandler struct {productService service.ProductService // 它依赖于 ProductService 接口
}// NewProductHandler 是 ProductHandler 的构造函数
func NewProductHandler(svc service.ProductService) *ProductHandler {return &ProductHandler{productService: svc}
}// CreateProduct godoc
// @Summary      创建一个新产品
// @Description  根据传入的JSON数据创建一个新产品
// @Tags         Products
// @Accept       json
// @Produce      json
// @Param        product  body      CreateProductRequest  true  "创建产品请求"
// @Success      201      {object}  models.Product
// @Failure      400      {object}  gin.H
// @Failure      500      {object}  gin.H
// @Router       /products [post]
func (h *ProductHandler) CreateProduct(c *gin.Context) {// 定义一个临时的结构体来绑定请求的JSON bodyvar req struct {Name  string  `json:"name" binding:"required"`Price float64 `json:"price" binding:"gt=0"`Stock int     `json:"stock" binding:"gte=0"`}// 解析并验证JSON请求体if err := c.ShouldBindJSON(&req); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data: " + err.Error()})return}// 调用Service层来创建产品product, err := h.productService.CreateProduct(c.Request.Context(), req.Name, req.Price, req.Stock)if err != nil {// 根据Service层返回的错误类型,可以返回更具体的HTTP状态码c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}// 返回201 Created状态码和创建成功的产品信息c.JSON(http.StatusCreated, product)
}// GetProduct godoc
// @Summary      获取单个产品
// @Description  根据产品ID获取产品详情
// @Tags         Products
// @Produce      json
// @Param        id   path      int  true  "产品ID"
// @Success      200  {object}  models.Product
// @Failure      400  {object}  gin.H
// @Failure      404  {object}  gin.H
// @Failure      500  {object}  gin.H
// @Router       /products/{id} [get]
func (h *ProductHandler) GetProduct(c *gin.Context) {// 从URL路径中获取ID参数id, err := strconv.ParseInt(c.Param("id"), 10, 64)if err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid product ID"})return}// 调用Service层获取产品product, err := h.productService.GetProduct(c.Request.Context(), id)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}// 如果Service层返回nil,说明产品不存在if product == nil {c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})return}c.JSON(http.StatusOK, product)
}// GetAllProducts godoc
// @Summary      获取所有产品列表
// @Description  获取数据库中所有产品的列表
// @Tags         Products
// @Produce      json
// @Success      200  {array}   models.Product
// @Failure      500  {object}  gin.H
// @Router       /products [get]
func (h *ProductHandler) GetAllProducts(c *gin.Context) {products, err := h.productService.GetAllProducts(c.Request.Context())if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}c.JSON(http.StatusOK, products)
}// UpdateProduct godoc
// @Summary      更新一个产品
// @Description  根据ID和传入的JSON数据更新一个已存在的产品
// @Tags         Products
// @Accept       json
// @Produce      json
// @Param        id       path      int                   true  "产品ID"
// @Param        product  body      UpdateProductRequest  true  "更新产品请求"
// @Success      200      {object}  models.Product
// @Failure      400      {object}  gin.H
// @Failure      404      {object}  gin.H
// @Failure      500      {object}  gin.H
// @Router       /products/{id} [put]
func (h *ProductHandler) UpdateProduct(c *gin.Context) {id, err := strconv.ParseInt(c.Param("id"), 10, 64)if err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid product ID"})return}var req struct {Name  string  `json:"name" binding:"required"`Price float64 `json:"price" binding:"gt=0"`Stock int     `json:"stock" binding:"gte=0"`}if err := c.ShouldBindJSON(&req); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data: " + err.Error()})return}product, err := h.productService.UpdateProduct(c.Request.Context(), id, req.Name, req.Price, req.Stock)if err != nil {// 这里可以根据service返回的错误类型判断是404还是500c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}c.JSON(http.StatusOK, product)
}// DeleteProduct godoc
// @Summary      删除一个产品
// @Description  根据ID删除一个产品
// @Tags         Products
// @Produce      json
// @Param        id   path      int  true  "产品ID"
// @Success      204  {object}  nil
// @Failure      500  {object}  gin.H
// @Router       /products/{id} [delete]
func (h *ProductHandler) DeleteProduct(c *gin.Context) {id, err := strconv.ParseInt(c.Param("id"), 10, 64)if err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid product ID"})return}err = h.productService.DeleteProduct(c.Request.Context(), id)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}// 对于删除操作,成功后通常返回 204 No Contentc.Status(http.StatusNoContent)
}
第7步:实现全新的main.go

实现全新的main.go

package mainimport ("awesomeProject/internal/config""awesomeProject/internal/database""awesomeProject/internal/repository""awesomeProject/internal/service""awesomeProject/transport/http"_ "gorm.io/gorm""log""github.com/gin-gonic/gin"
)func main() {// 1. 加载配置cfg, err := config.Load("./configs/config.dev.yaml")if err != nil {log.Fatalf("FATAL: Failed to load config: %v", err)}// 2. 初始化GORM数据库连接// 我们将GORM的初始化逻辑也封装到了database包中,使main.go更整洁db, err := database.NewGormConnection(cfg.Database)if err != nil {log.Fatalf("FATAL: Failed to connect to database: %v", err)}log.Println("Database connection established successfully.")// 3. 依赖注入:将所有组件连接起来// 数据流向: Handler -> Service -> Repository -> Database// a. 创建 Repository 实例,它依赖 GORM 的数据库连接(db)productRepo := repository.NewProductRepository(db)// b. 创建 Service 实例,它依赖 Repository 层的接口(productRepo)productService := service.NewProductService(productRepo)// c. 创建 Handler 实例,它依赖 Service 层的接口(productService)productHandler := http.NewProductHandler(productService)// 4. 初始化 Gin 路由引擎router := gin.Default()// 5. 注册产品相关的路由// 创建一个API分组,方便管理版本,例如 /api/v1apiV1 := router.Group("/api/v1"){products := apiV1.Group("/products"){products.POST("", productHandler.CreateProduct)       // 创建产品products.GET("", productHandler.GetAllProducts)       // 获取所有产品products.GET("/:id", productHandler.GetProduct)       // 获取单个产品products.PUT("/:id", productHandler.UpdateProduct)    // 更新产品products.DELETE("/:id", productHandler.DeleteProduct) // 删除产品}}// 6. 启动服务器log.Println("Starting server on port :8080")if err := router.Run(":8080"); err != nil {log.Fatalf("FATAL: Failed to start server: %v", err)}
}
接口调用测试

添加商品成功

请添加图片描述

查询商品

请忽略中间的参数,懒得没删除而已

请添加图片描述

查询全部商品

请添加图片描述

更新编号为1的商品

更新前:

请添加图片描述

更新后:

请添加图片描述

删除编号为1的商品:

请添加图片描述

删除后查询

请添加图片描述

数据库建设

products.sql

-- auto-generated definition
create table products
(id         bigint unsigned auto_incrementprimary key,created_at datetime(3)      null,updated_at datetime(3)      null,deleted_at datetime(3)      null,name       varchar(255)     not null,price      decimal(10, 2)   null,stock      bigint default 0 null
);create index idx_products_deleted_aton products (deleted_at);

数据表说明:

products: GORM 默认会将结构体名称 Product 转换为蛇形复数形式作为表名。

gorm.Model: 嵌入的 gorm.Model 自动为添加了 id, created_at, updated_at, deleted_at 四个核心字段。deleted_at 用于实现 GORM 的软删除功能。

请添加图片描述

总结

通过将 GORM 集成到我们的分层架构中,我们实现了一个完美的平衡:

  • 获得了 ORM 带来的高开发效率:告别了繁琐的 SQL 编写和手动映射。
  • 保留了清晰的架构和解耦:各层职责分明,易于维护和测试。

有用的网站:

  • GORM 指南 | GORM 中文文档

代码仓库:

🌍代码框架链接


http://www.dtcms.com/a/272444.html

相关文章:

  • selenium跳转到新页面时如何进行定位
  • 前缀和|差分
  • S7-1200 与 S7-300 PNS7-400 PN UDP 通信 TIA 相同项目
  • 缓存一致性问题(Cache Coherence Problem)是什么?
  • 使用Word/Excel管理需求的10个痛点及解决方案Perforce ALM
  • Word中字号与公式字体磅值(pt)的对应关系
  • 【AI智能体】智能音视频-通过关键词打断语音对话
  • RuoYi-Cloud ruoyi-gateway 网关模块
  • 海外盲盒系统:技术如何重构“信任经济”?
  • LLM 微调:从数据到部署的全流程实践与经验分享
  • 前端开发资源压缩与请求优化
  • FFmpeg滤镜相关的重要结构体
  • mongodbcdc脚本开发
  • 书生大模型实战营——1. 大语言模型原理与书生大模型提示词工程实践
  • 大数据学习7:Azkaban调度器
  • 记一次Android Studio编译报错:Execution failed for task ‘:app:compileDebugAidl‘
  • Redis数据类型之hash
  • Android 网络开发核心知识点
  • ICML 2025|快手提出了基于残差的超低码率图像压缩方法ResULIC
  • 【Bluedroid】蓝牙协议栈控制器能力解析与核心功能配置机制(decode_controller_support)
  • git中的fork指令解释
  • Linux - firewall 防火墙
  • 强缓存和协商缓存详解
  • 机器学习核心算法:PCA与K-Means解析
  • Java从入门到精通!第三天(数组)
  • Shell 中的重定向
  • C++实习面试题
  • 如何看待java开发和AI的关系?
  • GO启动一个视频下载接口 前端可以边下边放
  • 【PyTorch】PyTorch中的数据预处理操作