golang判断结构体为空的问题

 更新时间:2023年02月16日 10:41:14   作者:a...Z  
这篇文章主要介绍了golang判断结构体为空的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

golang结构体怎么判断是否为空

golang结构体怎么判断为空?就是判断是否已经初始化

方法如下:

可以使用if objectA== (structname{}){ // your code },进行判断。

示例代码如下:

package main

import (
    "fmt"
    "reflect"
)


type A struct{
    name string
    age int
}


func (a A) IsEmpty() bool {
    return reflect.DeepEqual(a, A{})
}


func main() {
    var a A
    if a == (A{}) {  // 括号不能去
        fmt.Println("a == A{} empty")
    }

    if a.IsEmpty() {
        fmt.Println("reflect deep is empty")
    }
}

golang 空接口 空结构体

空接口

  • 空接口是接口类型的特殊形式。空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度来看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
  • 空接口类型类似于 C# 或 Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法

空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

空接口类型的变量可以存储任意类型的变量。即使是接收指针类型也用 interface{},而不是使用 *interface{}。

永远不要使用一个指针指向一个接口类型,因为它已经是一个指针。

package main
 
import "fmt"
 
func main() {
    // 定义一个空接口x
    var x interface{}
    s := "pprof.cn"
    x = s
    fmt.Printf("type:%T value:%v\n", x, x)
    i := 100
    x = i
    fmt.Printf("type:%T value:%v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value:%v\n", x, x)
}

空接口内存分配 

​Go 1.15 中 var i interface{} = a 会有额外堆内存分配吗?

var a  int = 3
// 以下有额外内存分配吗?
var i interface{} = a

​Go 1.15在 runtime 部分中提到了一个有趣的改进:将小整数转换为接口值不再需要进行内存分配。小整数是指 0 到 255 之间的数。

空接口的应用

1.空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}
 
//函数的参数个数和每个参数的类型都不是固定的
func myfunc(args ...interface{}) {
}

2.空接口作为map,数组,切片的各种类型的值

func main() {
    // 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "李白"     //string
    studentInfo["age"] = 18        //int
    studentInfo["height"] = 1.82   //float
    studentInfo["married"] = false //bool
    fmt.Println(studentInfo)
 
    var a = new([3]interface{})
    a[0] = "Hello,World"
    a[1] = 32
    for _, b := range a {
        fmt.Printf("%v\t%[1]T\n", b)
    }
}

3.类型断言

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

func main() {
    var x interface{}
    x = "pprof.cn"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

4.类型判断

func justifyType(any interface{}) {
    switch v := any.(type) {
    case string:
        fmt.Printf("any is a string,value is: %v\n", v)
    case int:
        fmt.Printf("any is a int is: %v\n", v)
    case bool:
        fmt.Printf("any is a bool is: %v\n", v)
    case float32, float64:
        fmt.Printf("any is a float is: %v\n", v)
    default:
        fmt.Printf("unsupport type:%T is: %v\n", v, v)
    }
}
 
func main() {
    var x interface{}
    x = "pprof.cn"
    justifyType(x)
    x = 0.1
    justifyType(x)
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

空结构体

我们说不包含任何字段的结构体叫做空结构体,可以通过如下的方式定义空结构体:

type empty struct{}

特点

地址相同

我们分别定义两个非空结构体和空结构体变量,然后取地址打印,发现空结构体变量的地址是相同的:

// 定义一个非空结构体
type User struct {
    name string
}
 
func main() {
    // 两个非空结构体的变量地址不同
    var user1 User
    var user2 User
    fmt.Printf("%p \n", &user1) // 0xc000318670
    fmt.Printf("%p \n", &user2) // 0xc000318680
 
    // 定义两个空结构体,地址相同
    var first struct{}
    var second struct{}
    fmt.Printf("%p \n", &first)  // 0x1ca15f0
    fmt.Printf("%p \n", &second) // 0x1ca15f0
}

我们知道 Go 语言中的变量传递都是值传递,对于传参前后的变量地址应该不同,我们通过传参的方式再来试一下:

// 非空结构体
type NonEmptyUser struct {
    name string
}
 
// 空结构体
type EmptyUser struct{}
 
// 打印非空结构体参数地址
func testNonEmptyUser(user NonEmptyUser) {
    fmt.Printf("%p \n", &user)
}
 
// 打印空结构体参数地址
func testEmptyUser(user EmptyUser) {
    fmt.Printf("%p \n", &user)
}
 
func main() {
    // 两个非空结构体的变量地址不同
    var user1 NonEmptyUser
    fmt.Printf("%p \n", &user1) // 0xc0001986c0
    testNonEmptyUser(user1)     // 0xc0001986d0
 
    // 两个空结构体变量的地址相同
    var user2 EmptyUser
    fmt.Printf("%p \n", &user2) // 0x1ca25f0
    testEmptyUser(user2)        // 0x1ca25f0
}

发现对于非空结构体,传参前后的地址是不同的,但是对于空结构体变量,前后地址是一致的。

内存占用大小为0

在Go中,我们可以使用 unsafe.Sizeof 来计算一个变量占用的字节数,那么就举几个例子来看下:

type EmptyUser struct{}
 
func main() {
    var i int
    var s string
    var m []string
    var u EmptyUser
  
    fmt.Println(unsafe.Sizeof(i)) // 8
    fmt.Println(unsafe.Sizeof(s)) // 16
    fmt.Println(unsafe.Sizeof(m)) // 24
    fmt.Println(unsafe.Sizeof(u)) // 0
}

可以看到空结构体占用的内存空间大小为0,同时对于空结构体的组合,占用空间大小也为0:

// 空结构体的组合
type EmptyUser struct {
    name struct{}
    age  struct{}
}
 
func main() {
    var u EmptyUser
    fmt.Println(unsafe.Sizeof(u)) // 0
}

原理探究

为什么空结构体的地址都相同,而且大小都为0呢,我们一起来看下源码(go/src/runtime/malloc.go):

// base address for all 0-byte allocations
var zerobase uintptr
 
// 创建新的对象时,调用 mallocgc 分配内存
func newobject(typ *_type) unsafe.Pointer {
    return mallocgc(typ.size, typ, true)
}
 
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    if gcphase == _GCmarktermination {
        throw("mallocgc called with gcphase == _GCmarktermination")
    }
 
    if size == 0 {
        return unsafe.Pointer(&zerobase)
    }
    ......
}

通过源码可以看出,创建新的对象时,需要调用 malloc.newobject() 进行内存分配,进一步调用 mallocgc 方法,在该方法中,如果判断类型的size==0 ,固定返回zerobase的地址。

zerobase是一个uintptr 全局变量,占用 8 个字节。因此我们可以确定的是,在Go语言中,所有针对 size==0 的内存分配,用的都是同一个地址 &zerobase,所以我们在一开始看到的所有空结构体地址都相同。

使用场景

空结构体不包含任何数据,那么其应用场景也应该不在乎值内容,只当做一个占位符。在这种场景下,由于其不占用内存空间,使用空结构体既可以做到节省空间,又可以提供语义支持。

集合(Set)

使用过 Java 的同学应该都用过 Set 类型,Set 是保存不重复元素的集合,但是 Go 语言没有提供原生的 Set 类型。

但是我们知道 Map 结构存储的是 key-value 类型,key 不允许重复,因此可以利用 Map 来实现 Set,key存储需要的数据,value 给个固定值就可以了。

那么 value 给什么值好呢?这时候我们的 空结构体 就可以出场了,不占用空间,还可以完成占位操作,堪称完美,下面我们看怎么实现吧。

比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。

// 定义了一个保存 string 类型的 Set集合
type Set map[string]struct{}
 
// 添加一个元素
func (s Set) Add(key string) {
    s[key] = struct{}{} //第2个{}表示赋值
}
 
// 移除一个元素
func (s Set) Remove(key string) {
    delete(s, key)
}
 
// 是否包含一个元素
func (s Set) Contains(key string) bool {
    _, ok := s[key]
    return ok
}
 
// 初始化
func NewSet() Set {
    s := make(Set)
    return s
}
 
// 测试使用
func main() {
    set := NewSet()
    set.Add("hello")
    set.Add("world")
    fmt.Println(set.Contains("hello"))
 
    set.Remove("hello")
    fmt.Println(set.Contains("hello"))
}
 
func min(a int, b uint) {
    var min = 0
    if a < 0 {
        min = a
    } else {
        min = copy(make([]struct{}, a), make([]struct{}, b))
    }
    fmt.Printf("The min of %d and %d is %d\n", a, b, min)
}

channel中信号传输

空结构体 与 channel 可谓是一个经典组合,有时候我们只是需要一个信号来控制程序的运行逻辑,并不在意其内容如何。

在下面的例子中,我们定义了两个 channel 用于接收两个任务完成的信号,当接收到任务完成的信号时,就会触发相应的动作。

func doTask1(ch chan struct{}) {
    time.Sleep(time.Second)
    fmt.Println("do task1")
    ch <- struct{}{}
}
 
func doTask2(ch chan struct{}) {
    time.Sleep(time.Second * 2)
    fmt.Println("do task2")
    ch <- struct{}{}
}
 
func main() {
    ch1 := make(chan struct{})
    ch2 := make(chan struct{})
    go doTask1(ch1)
    go doTask2(ch2)
 
    for {
        select {
        case <-ch1:
            fmt.Println("task1 done")
        case <-ch2:
            fmt.Println("task2 done")
        case <-time.After(time.Second * 5):
            fmt.Println("after 5 seconds")
            return
        }
    }
}

本篇文章,我们学习了如下内容:

  • 空结构体是一种特殊的结构体,不包含任何元素
  • 空结构体的大小都为0
  • 空结构体的地址都相同
  • 由于空结构体不占用空间,从节省内存的角度出发,适用于实现Set结构、在 channel 中传输信号等

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Go连接并操作SQLite数据库基本步骤

    Go连接并操作SQLite数据库基本步骤

    在这篇文章中,我们将详细描述如何在 Go 语言中使用 SQLite 数据库,我们将会从如何在 Go 中安装和使用 SQLite 驱动包开始讲起,然后逐步介绍如何创建数据库连接,执行 SQL 查询,处理返回的数据以及关闭数据库连接
    2024-01-01
  • Go语言展现快速排序算法全过程的思路及代码示例

    Go语言展现快速排序算法全过程的思路及代码示例

    这篇文章主要介绍了Go语言展现快速排序算法全过程的思路及代码示例,文章最后作者还提到了对Quick Sort算法优化的一些想法,需要的朋友可以参考下
    2016-04-04
  • Golang 中的 strconv 包常用函数及用法详解

    Golang 中的 strconv 包常用函数及用法详解

    strconv是Golang中一个非常常用的包,主要用于字符串和基本数据类型之间的相互转换,这篇文章主要介绍了Golang中的strconv包,需要的朋友可以参考下
    2023-06-06
  • Go语言数据结构之希尔排序示例详解

    Go语言数据结构之希尔排序示例详解

    这篇文章主要为大家介绍了Go语言数据结构之希尔排序示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • golang构建HTTP服务的实现步骤

    golang构建HTTP服务的实现步骤

    其实很多框架都是在 最简单的http服务上做扩展的的,基本上都是遵循http协议,本文主要介绍了golang构建HTTP服务,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • 使用systemd部署和守护golang应用程序的操作方法

    使用systemd部署和守护golang应用程序的操作方法

    systemd是一个流行的守护进程管理器,可以轻松管理服务的启动、停止、重启等操作,让我们的应用程序始终保持在线,本文介绍了如何使用systemd部署和守护golang应用程序,感兴趣的朋友一起看看吧
    2023-10-10
  • golang多维度排序及题解最长连续序列

    golang多维度排序及题解最长连续序列

    这篇文章主要为大家介绍了golang多维度排序及题解最长连续序列示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • Golang发送http GET请求的示例代码

    Golang发送http GET请求的示例代码

    这篇文章主要介绍了Golang发送http GET请求的示例代码,帮助大家更好的理解和使用golang,感兴趣的朋友可以了解下
    2020-12-12
  • 详解Golang时间处理的踩坑及解决

    详解Golang时间处理的踩坑及解决

    在各个语言之中都有时间类型的处理,这篇文章主要和大家分享一下Golang进行时间处理时哪里最容易踩坑以及解决方法,需要的可以参考一下
    2023-01-01
  • Golang httptest包测试使用教程

    Golang httptest包测试使用教程

    这篇文章主要介绍了Golang httptest包测试使用,httptest包的理念是,非常容易模拟http服务,也就是说模拟响应写(response writer),提供给http处理器(handle),让我们测试http服务端和客户端很容易
    2023-03-03

最新评论