【Golang】sql.Null* 类型使用(处理空值和零值)

sql.NullStringsql.NullInt64 类型(以及其他类似的 sql.Null* 类型)在处理数据库操作时非常有用,尤其是在 Go 语言的 database/sql 包中。它们的主要用途包括:

  1. 表示 NULL 值

    • 在数据库中,NULL 表示"没有值"。sql.Null* 类型允许你在 Go 语言中直接表示和处理这种"没有值"的状态,而不是使用 Go 的零值(如空字符串 "" 或整数 0)来表示 NULL,这样可以更准确地反映数据库中的数据状态。
  2. 数据库交互

    • 当你从数据库查询数据时,某些字段可能是 NULL。使用 sql.Null* 类型可以区分一个字段是真正包含零值还是 NULL。这对于后续的逻辑处理非常重要,因为零值和 NULL 在逻辑上可能有不同的含义。
  3. 避免错误

    • 如果不使用 sql.Null* 类型,你可能会错误地将数据库中的 NULL 值解释为 Go 语言中的零值,这可能导致逻辑错误或程序崩溃。
  4. 简化代码

    • 使用 sql.Null* 类型可以简化代码,因为你不需要编写额外的检查来确定一个值是否来自数据库中的 NULL。Valid 字段可以直接告诉你这个值是否有效。
  5. 数据完整性

    • 在将数据写入数据库时,sql.Null* 类型可以帮助你保持数据的完整性。你可以准确地标记哪些字段是有意设置为 NULL,哪些字段是具有实际值的。
  6. 灵活性

    • sql.Null* 类型提供了灵活性,允许你在不改变现有代码结构的情况下,处理那些可能为 NULL 的字段。

示例场景:

假设你正在处理一个用户信息表,其中有一个字段表示用户的出生日期。在某些情况下,用户可能没有提供出生日期,因此这个字段在数据库中是 NULL。使用 sql.NullStringsql.NullInt64(取决于出生日期如何存储)可以让你在 Go 程序中准确地表示这种情况,并在需要时进行适当的处理。

复制代码
package main

import (
	"database/sql"
	"fmt"
)

// DbBackedUser 用户结构体,由数据库支持
type DbBackedUser struct {
	Name sql.NullString
	Age  sql.NullInt64
}

func main() {
	// 空值
	var name sql.NullString
	x := DbBackedUser{Name: name, Age: sql.NullInt64{Int64: 0, Valid: false}}

	// 输出结果
	fmt.Println("Name:")
	fmt.Println("String:", x.Name.String) // 输出: ""
	fmt.Println("Valid:", x.Name.Valid)   // 输出: false

	fmt.Println("Age:")
	fmt.Println("Int64:", x.Age.Int64)  // 输出: 0
	fmt.Println("Valid:", x.Age.Valid)   // 输出: false
}

Name:
String: 
Valid: false
Age:
Int64: 0
Valid: false

加入与数据库交互

1. 创建测试表

clickhouse-client

首先,我们需要在 ClickHouse 中创建一个测试表。因为你的数据结构包含 NameAge,我们可以使用以下 SQL 语句创建表:

复制代码
CREATE TABLE test_table
(
    `Name` Nullable(String),
    `Age`  Nullable(Int64)
) ENGINE = Memory;

2. 插入数据

接下来,我们插入一些数据,包括空值和零值:

复制代码
INSERT INTO test_table VALUES ('Kimi', 30), ('', 0), (NULL, NULL);

这里,我们插入了三行数据:

  • 第一行:Name 是 "Kimi",Age 是 30。
  • 第二行:Name 是空字符串,Age 是 0。
  • 第三行:NameAge 都是 NULL。

3. 查询数据

查询表中的所有数据:

复制代码
SELECT * FROM test_table;
复制代码
package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/ClickHouse/clickhouse-go"
)

// DbBackedUser 用户结构体,由数据库支持
type DbBackedUser struct {
	Name sql.NullString
	Age  sql.NullInt64
}

func main() {
	// 连接到 ClickHouse 数据库
	connStr := "tcp://localhost:9000?username=default&password=&database=default"
	db, err := sql.Open("clickhouse", connStr)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 查询并打印现有的数据
	fmt.Println("Query results before insertion:")
	queryData(db)
	fmt.Println("查询并打印现有的数据")

	// 开始事务
	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}

	// 空值
	var name sql.NullString
	x := DbBackedUser{Name: name, Age: sql.NullInt64{Int64: 1, Valid: false}}

	// 插入数据
	_, err = tx.Exec("INSERT INTO test_table2 (Name, Age) VALUES (?, ?)", x.Name, x.Age)
	if err != nil {
		tx.Rollback() // 如果出现错误,回滚事务
		log.Fatal(err)
	}

	// 提交事务
	err = tx.Commit()
	if err != nil {
		log.Fatal(err)
	}

	// 查询并打印插入后的数据
	fmt.Println("Query results after insertion:")
	queryData(db)
	fmt.Println("查询并打印现有的数据")

}

func queryData(db *sql.DB) {
	rows, err := db.Query("SELECT Name, Age FROM test_table2")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	var nameValue sql.NullString
	var ageValue sql.NullInt64
	for rows.Next() {
		if err := rows.Scan(&nameValue, &ageValue); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("name: %v, Age: %v\n", nameValue.String, ageValue.Int64)
	}
}

结果

这里

给数据库传了0值

改成true后

传值成功

NameValid改成false

传null

复制代码
package main

import (
	"database/sql"
	"fmt"
	"reflect"

	"github.com/go-playground/validator/v10"
)

// DbBackedUser 用户结构体,由数据库支持
type DbBackedUser struct {
	Name sql.NullString `validate:"required"` // 可以为NULL的字符串,验证规则为必填
	Age  sql.NullInt64  `validate:"required"` // 可以为NULL的整数,验证规则为必填
}

// 使用单一实例的验证器,它缓存结构体信息
var validate *validator.Validate

func main() {
	// 创建一个新的验证器实例
	validate = validator.New()

	// 注册所有sql.Null*类型,使用自定义的ValidateValuer函数
	validate.RegisterCustomTypeFunc(ValidateValuer, sql.NullString{}, sql.NullInt64{})

	// 构建对象进行验证
	// Name字段为空字符串但Valid为true,表示非NULL;Age字段为0且Valid为false,表示NULL
	x := DbBackedUser{Name: sql.NullString{String: "zahngsan", Valid: true}, Age: sql.NullInt64{Int64: 18, Valid: false}}

	// 对x进行结构体验证
	err := validate.Struct(x)

	// 如果验证失败,打印错误信息
	if err != nil {
		fmt.Printf("Err(s):\n%+v\n", err)
	}
}

// ValidateValuer实现了validator.CustomTypeFunc接口
func ValidateValuer(field reflect.Value) interface{} {
	// 检查field是否为nil
	if !field.IsValid() {
		return nil
	}

	// 检查field是否实现了sql.Null*类型
	if nullType, ok := field.Interface().(sql.NullString); ok {
		// 如果Valid为true,返回字符串和非NULL的指示
		if nullType.Valid {
			fmt.Println("NullString Valid为true:",nullType.String)
			return []interface{}{nullType.String, true}
		}
		// 如果Valid为false,返回空字符串和NULL的指示
		fmt.Println("NullString Valid为false")
		return []interface{}{"", false}
	}

	// 检查field是否实现了sql.NullInt64类型
	if nullInt64, ok := field.Interface().(sql.NullInt64); ok {
		if nullInt64.Valid {
			fmt.Println("NullInt64 Valid为true:",nullInt64.Int64)
			return []interface{}{nullInt64.Int64,true}
		}
		fmt.Println("NullInt64 Valid为false")
		return []interface{}{"", false}
	}

	// 对其他sql.Null*类型进行类似的处理...
	// ...

	// 如果不是sql.Null*类型,返回nil
	return nil
}

结果

相关推荐
ZWZhangYu18 分钟前
LangChain 构建向量数据库和检索器
数据库·langchain·easyui
feifeigo1231 小时前
升级到MySQL 8.4,MySQL启动报错:io_setup() failed with EAGAIN
数据库·mysql·adb
火龙谷3 小时前
【nosql】有哪些非关系型数据库?
数据库·nosql
焱焱枫4 小时前
Oracle获取执行计划之10046 技术详解
数据库·oracle
双力臂4045 小时前
MyBatis动态SQL进阶:复杂查询与性能优化实战
java·sql·性能优化·mybatis
qq_392397125 小时前
Redis常用操作
数据库·redis·wpf
A__tao6 小时前
一键将 SQL 转为 Java 实体类,全面支持 MySQL / PostgreSQL / Oracle!
java·sql·mysql
一只fish7 小时前
MySQL 8.0 OCP 1Z0-908 题目解析(17)
数据库·mysql
花好月圆春祺夏安7 小时前
基于odoo17的设计模式详解---装饰模式
数据库·python·设计模式
A__tao7 小时前
SQL 转 Java 实体类工具
java·数据库·sql