Go语言入门之路——数据结构

简介: Go语言入门之路——数据结构

切片与数组

前言

在Go中,数组和切片两者看起来长得几乎一模一样,但功能有着不小的区别,数组是定长的数据结构,长度被指定后就不能被改变,而切片是不定长的,切片在容量不够时会自行扩容。

数组

如果我们事先就知道了要存放数据的长度,而且我们在后续使用中不会有扩容的需求,我们就可以考虑使用数组

注意:在Go中,数组是值类型而非引用,它并不是指向数组头部元素的指针

数组的初始化

数组在上面长度时只能是一个常量,而绝不能是一个变量

var a[5] int  //正确定义
//错误示范
l:=1
var b[l] int

数组的初始化写法一般有以下几种:

  1. 只初始化长度
var a[5] int
  1. 利用元素初始化
a:=[5]int{1,2.3}
  1. 利用new获得一个指针
num:=new([5]int)
  1. 以上几种方式都会给nums分配一片固定大小的内存,区别只是最后一种得到的值是指针
数组的使用

只要有数组名和下标,就可以访问数组中对应的元素。

fmt.Println(nums[0])

同样的也可以修改数组元素

nums[0] = 1

还可以通过内置函数len来访问数组元素的数量

len(nums)

内置函数cap来访问数组容量,数组的容量等于数组长度,容量对于切片才有意义。

cap(nums
数组的切割

切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开,例子如下:

nums := [5]int{1, 2, 3, 4, 5}
nums[1:] // 子数组范围[1,5) ->2 3 4 5
nums[:5] // 子数组范围[0,5) -> 1 2 3 4 5
nums[2:3] // 子数组范围[2,3) -> 3
nums[1:3] // 子数组范围[1,3) -> 2 3

切片

切片在Go中应用的更加广泛一些,用于存放不知道长度的数据,且后续使用过程中可能会频繁的插入和删除元素(我个人觉得比较像STL容器中的vector)

切片的初始化

切片的初始化主要有以下几种方式:

var a[]int
a:=[]int{1,2,3}
a:=make([]int,0,0)
a:=new([]int)

可以看到切片与数组在外貌上的区别,仅仅只是少了一个初始化长度。通常情况下,推荐使用make来创建一个空切片,只是对于切片而言,make函数接收三个参数:类型,长度,容量。切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会。

注意

  1. 切片的底层实现依旧是数组,是引用类型。我们可以将它理解为指向底层数组的指针
  2. 通过var nums []int这种方式声明的切片,默认值为nil,所以不会为其分配内存,而在使用make进行初始化时,建议预分配一个足够的容量,可以有效减少后续扩容的内存消耗
向切片里面插入元素

切片的插入元素主要是基于append函数来实现的,append的函数签名是这样的:

func append(slice []Type,elems ...Type)

说明:

slice:待添加元素的切片

elems:添加的元素

接下来,给大家演示一个简单的demo:

package main
import "fmt"
func main() {
  a:=[]int{1,2,3,4,5}
  a=append(a,[]int {6,7,8,9,10}...)
  for i,v:=range a{
    fmt.Printf("a[%d]=%d\n",i,v)
  }
}

上面简单的展示了以下,如何利用append函数插入一个另一个切片,值得注意的是:这里的省略号不能省略,它的作用是展开我们要插入切片,否则会出现下面这个报错:

当然我们也可以来选择插入元素的位置:

  • 从头部插入
package main
import "fmt"
func main() {
  a:=[]int{1,2,3,4,5}
  a=append(a,[]int {6,7,8,9,10}...)
  for i,v:=range a{
    fmt.Printf("a[%d]=%d\n",i,v)
  }
}
  • 从指定下标i插入
package main
import "fmt"
func main() {
  nums := make([]int, 0, 0)
  nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
  i:=3
  nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
  for i,v:=range nums{
    fmt.Printf("nums[%d]=%d\n",i,v)
  }
}
  • 尾部插入元素
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]
删除切片元素

从头部删除n个元素

nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]

从尾部删除n个元素

nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]

从中间指定下标i位置开始删除n个元素

nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]

删除所有元素

nums = nums[:0]
fmt.Println(nums) // []
拷贝

切片在拷贝时需要确保目标切片有足够的长度,例如

func main() {
  dest := make([]int, 0)
  src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  fmt.Println(src, dest)
  fmt.Println(copy(dest, src))
  fmt.Println(src, dest)
}
[1 2 3 4 5 6 7 8 9] []
0                     
[1 2 3 4 5 6 7 8 9] []

将长度修改为10,输出如下

[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9                                        
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]
多维切片和多维数组
  • 数组的初始化
var a [5][5]int
- 切片的初始化
 ```go
 slice=make([][]int,5)
 for i;i<len(slice);i++:{
     slice[i]=make([]int,5)
 }

切片与数组都可以使用简单表达式来进行切割,但是拓展表达式只有切片能够使用,该特性于Go1.2版本添加,主要是为了解决切片共享底层数组的读写问题,主要格式为如下,需要满足关系low<= high <= max <= cap,使用拓展表达式切割的切片容量为max-low

slice[low:high:max]
low`与`high`依旧是原来的含义不变,而多出来的`max`则指的是最大容量,例如下方的例子中省略了`max`,那么`s2`的容量就是`cap(s1)-low
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6

那么这么做就会有一个明显的问题,s1s2是共享的同一个底层数组,在对s2进行读写时,有可能会影响的s1的数据,下列代码就属于这种情况

s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
// 添加新元素,由于容量为6.所以没有扩容,直接修改底层数组
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)

最终的输出为

[4 1]
[1 2 3 4 1 6 7 8 9]

可以看到明明是向s2添加元素,却连s1也一起修改了,拓展表达式就是为了解决此类问题而生的,只需要稍微修改一下就能解决该问题

func main() {
   s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
   s2 := s1[3:4:4]                        // cap = 4 - 3 = 1
   // 容量不足,分配新的底层数组
   s2 = append(s2, 1)
   fmt.Println(s2)
   fmt.Println(s1)
}

现在得到的结果就是正常的

[4 1]
[1 2 3 4 5 6 7 8 9]
clear

在go1.21新增了clear内置函数,clear会将切片内所有的值置为零值,

package main
import (
    "fmt"
)
func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

输出

[0 0 0 0]

如果想要清空切片,可以

func main() {
  s := []int{1, 2, 3, 4}
    s = s[:0:0]
  fmt.Println(s)
}

限制了切割后的容量,这样可以避免覆盖原切片的后续元素。

字符串

在Go中,字符串本质上是一个不可变的只读的字节数组,也是一片连续的内存空间。

原生字符串和普通字符串

  • 普通字符串
    ""双引号表示,支持转义,不支持多行书写
  • 原生字符串
    由反引号表示,不支持转义,支持多行书写,原生字符串里面所有的字符都会原封不动的输出,包括换行和缩进

接下来用一个很简单的demo给大家演示一下:

package main
import "fmt"
func main() {
  s1:="1235468976\nover"
  s2:=`1234567890\nover`
  fmt.Println(s1,s2)
}

字符串的访问

由于字符串的本质是字节数组,所以字符串的访问形式与数组切片完全一致,比如下面这个demo:

package main
import "fmt"
func main() {
  s2:=`1234567890\nover`
  fmt.Println(s2[3])
}

它的输出为:

我们可以看到输出的是字节而不是字符

切割字符串

package main
import "fmt"
func main() {
  s2:=`1234567890\nover`
  fmt.Println(s2[1:4])
}

输出为:

覆盖字符串

尝试修改字符串元素

func main() {
   str := "this is a string"
   str[0] = 'a' // 无法通过编译
   fmt.Println(str)
}
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}

字符串的转换

字符串可以转换为字节切片,而字节切片也可以转换为

字符串,示例如下:

package main
import "fmt"
func main() {
  s1:="abcdefg"
  bytes:=[]byte(s1)
  fmt.Println(bytes)
  s2:=string(bytes)
  fmt.Println(s2)
}

输出为:

字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的。

func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // 修改字节切片
  bytes = append(bytes, 96, 97, 98, 99)
    // 赋值给原字符串
  str = string(bytes)
  fmt.Println(str)
}

注意:两种类型之间的转换都需要进行数据拷贝,其性能损耗会随着长度的增加而增长

字符串拷贝

字符串的拷贝其实类似于数组切片的拷贝,而字符串拷贝其实本质上是字节切片拷贝,它的实现方式主要有以下两种,接下来我会用两个小demo来演示一下:

第一种方法

package main
import "fmt"
func main(){
  var s1,s2 string
  s1="fengxu"
  bytes:=make([]byte,len(s1))
  copy(bytes,s1)
  s2=string(bytes)
  fmt.Println(s2)
}

当然我们也可以使用strings.Clone函数package main

package main
import(
  "fmt"
  "strings"
) 
func main(){
  var s1,s2 string
  s1="fengxu"
  s2=strings.Clone(s1)
  fmt.Println(s2)
}

字符串的拼接

字符串的拼接的方法主要有三种:

直接用+

package main
import(
  "fmt"
  _"strings"   //不适用该包用匿名导入
) 
func main(){
  var s1,s2 string
  s1="My name is"
  s2=s1+" fengxu"
  fmt.Println(s2)
}

转换为字符切片再加入元素

package main
import(
  "fmt"
  _"strings"
) 
func main() {
  s1 := "fengxu"
  bytes := make([]byte, len(s1))
  copy(bytes, s1)
  bytes = append([]byte("My name is "),bytes ...)
  fmt.Println(string(bytes))
}

但是其实这两种字符串的拼接性能都比较差,如果追求性能的话,我们可以考虑使用内置函数string.Builder

package main
import(
  "fmt"
  "strings"
) 
func main() {
  bulider:=strings.Builder{}
  bulider.WriteString("Hello,")
  bulider.WriteString(" World!")
  fmt.Println(bulider.String()) //输出:Hello, World
}

字符串的遍历

在开头我们就已经提到过,Go中的字符串就是一个只读的字节切片,也就是说字符串的组成单位是字节而不是字符。这种情况经常会在遍历字符串时遇到,例如下方的代码

func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

输出结构应该是这样的:

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,  
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,! 

其实对于字节而言输出大姑子姐的英文字母,其实问题并不大,但是我们要明确一点,在ASCII码中中文字符其实是三个字节,这样会使我们的结果出现偏差,Go字符串是明确支持utf8的,应对这种情况就需要用到rune类型,在使用for range进行遍历时,其默认的遍历单位类型就是一个rune,例如下方代码

func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

输出如下

104,68,h
101,65,e     
108,6c,l     
108,6c,l     
111,6f,o     
32,20,       
19990,4e16,世
30028,754c,界
33,21,!  

rune本质上是int32的类型别名,unicode字符集的范围位于0x0000 - 0x10FFFF之间,最大也只有三个字节,合法的UTF8编码最大字节数只有4个字节,所以使用int32来存储是理所当然,上述例子中将字符串转换成[]rune再遍历也是一样的道理,如下

func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

还可以使用uft8包下的工具,例如

func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

映射表

一般来说,映射表的数据结构实现主要有两种:哈希表(hash table)和搜索树(search tree),前者实现原理是基于哈希映射而后者则是红黑树,区别在于前者无序,后者有序,本篇旨在介绍常见的数据结构,故不做详细介绍,后面我会单开一篇文章来介绍其实现原理

map的初始化

在Go中,map的键类型必须是可以比较的,比如string,int,而像[]int这种不能比较的也就不能作为map的键,接下来我们来看一下map初始化的两种方法:

  • 直接用字面量来初始化
    形式主要为:
map[keyType]ValueType{}
• 1

示例如下:

mp := map[int]string{
   0: "a",
   1: "a",
   2: "a",
   3: "a",
   4: "a",
}
mp := map[string]int{
   "a": 0,
   "b": 22,
   "c": 33,
}
  • 利用make函数为其分配内存
    make函数分配内存会接受两个参数,分别是类型与初始容量,例子如下:
mp := make(map[string]int, 8)
mp := make(map[string][]int, 10)

注意:map是引用类型,零值或未初始化的map可以访问,但是无法存放元素,所以必须要为其分配内存

对映射表的访问

访问一个map的方式就像通过索引访问一个数组一样。

func main() {
  mp := map[string]int{
    "a": 0,
    "b": 1,
    "c": 2,
    "d": 3,
  }
  fmt.Println(mp["a"])
  fmt.Println(mp["b"])
  fmt.Println(mp["d"])
  fmt.Println(mp["f"])
}
0
1
3
0

通过代码可以观察到,即使map中不存在"f"这一键值对,但依旧有返回值。map对于不存的键其返回值是对应类型的零值,并且在访问map的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在,例如:

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   if val, exist := mp["f"]; exist {
      fmt.Println(val)
   } else {
      fmt.Println("key不存在")
   }
}

备注:这里我们来解释一下f val, exist := mp["f"]; exist这句代码的含义:

对map求长度

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(len(mp))
}

映射表的存值

map存值的方式也类似数组存值一样,例如:

func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   fmt.Println(mp)
}

存值时使用已存在的键会覆盖原有的值

func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   if _, exist := mp["b"]; exist {
      mp["b"] = 3
   }
   fmt.Println(mp)
}

但是也存在一个特殊情况,那就是键为math.NaN()

func main() {
  mp := make(map[float64]string, 10)
  mp[math.NaN()] = "a"
  mp[math.NaN()] = "b"
  mp[math.NaN()] = "c"
  _, exist := mp[math.NaN()]
  fmt.Println(exist)
  fmt.Println(mp)
}
false
map[NaN:c NaN:a NaN:b]

通过结果可以观察到相同的键值并没有覆盖,反而还可以存在多个,也无法判断其是否存在,也就无法正常取值。因为NaN是IEE754标准所定义的,其实现是由底层的汇编指令UCOMISD完成,这是一个无序比较双精度浮点数的指令,该指令会考虑到NaN的情况,因此结果就是任何数字都不等于NaN,NaN也不等于自身,这也造成了每次哈希值都不相同。关于这一点社区也曾激烈讨论过,但是官方认为没有必要去修改,所以应当尽量避免使用NaN作为map的键。

映射表键值对的删除

删除一个键值对需要用到内置函数delete,函数形式如下:

func delete(m map[Type]Type1, key Type)

示例代码如下:

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(mp)
   delete(mp, "a")
   fmt.Println(mp)
}

**注意:**如果值为NaN,甚至没法删除该键值对。

映射表的遍历

通过for range可以遍历map,例如

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
c 2
d 3
a 0
b 1
• 1
• 2
• 3
• 4

可以看到结果并不是有序的,也印证了map是无序存储。值得一提的是,NaN虽然没法正常获取,但是可以通过遍历访问到,例如

func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
NaN a
NaN c
NaN b

映射表的清空

在go1.21之前,想要清空map,就只能对每一个map的key进行delete

func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  for k, _ := range m {
    delete(m, k)
  }
  fmt.Println(m)
}

但是go1.21更新了clear函数,就不用再进行之前的操作了,只需要一个clear就可以清空

func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  clear(m)
  fmt.Println(m)
}

set

Set是一种无序的,不包含重复元素的集合,Go中并没有提供类似的数据结构实现,但是map的键正是无序且不能重复的,所以也可以使用map来替代set。

func main() {
  set := make(map[int]struct{}, 10)
  for i := 0; i < 10; i++ {
    set[rand.Intn(100)] = struct{}{}
  }
  fmt.Println(set)
}
map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]
• 1

映射表使用的注意事项

map并不是一个并发安全的数据结构,Go团队认为大多数情况下map的使用并不涉及高并发的场景,引入互斥锁会极大的降低性能,map内部有读写检测机制,如果冲突会触发fatal error。例如下列情况有非常大的可能性会触发fatal。在这种情况下,需要使用sync.Map来替代,这个我们在后续并发的文章中会提到

指针

前言

Go保留了指针,在一定程度上保证了性能,同时为了更好的GC(后面会介绍什么是GC)和安全角度考虑,又限制了指针的使用

指针的创建

指针的常用操作符主要有两个,一个是取地址符&,另一个是解引用符*

对一个变量进行取地址,会返回对应类型的指针,例如:

func main() {
   num := 2
   p := &num
   fmt.Println(p)
}

指针存储的是变量num的地址

0xc00001c088
• 1

解引用符则有两个用途,第一个是访问指针所指向的元素,也就是解引用,例如

func main() {
  num := 2
  p := &num
  rawNum := *p
  fmt.Println(rawNum)
}

p是一个指针,对指针类型解引用就能访问到指针所指向的元素。还有一个用途就是声明一个指针,例如:

func main() {
   var numPtr *int
   fmt.Println(numPtr)
}
<nil>

*int即代表该变量的类型是一个int类型的指针,不过指针不能光声明,还得初始化,需要为其分配内存,否则就是一个空指针,无法正常使用。要么使用取地址符将其他变量的地址赋值给该指针,要么就使用内置函数new手动分配,例如:

func main() {
   var numPtr *int
   numPtr = new(int)
   fmt.Println(numPtr)
}

更多的是使用短变量

func main() {
   numPtr := new(int)
   fmt.Println(numPtr)
}

new函数只有一个参数那就是类型,并返回一个对应类型的指针,函数会为该指针分配内存,并且指针指向对应类型的零值,例如:

func main() {
   fmt.Println(*new(string))
   fmt.Println(*new(int))
   fmt.Println(*new([5]int))
   fmt.Println(*new([]float64))
}
0          
[0 0 0 0 0]
[]  

禁止指针运算

什么是指针偏移

在Go里面是不允许指针运算的,也就是说在Go里面是不允许指针偏移的情况出现,或许大家不是很清楚什么是指针偏移,接下来未来大家简单了解一下:

首先我们知道在c++中,指针可以看作数组中头部元素的地址而我们可以让这个指针发生偏移进而得到其他元素,如下:

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int *p = &arr[0];
    cout << &arr << endl
         << p << endl
         << p + 1 << endl
         << &arr[1] << endl;
}

输出:

0x31d99ff880
0x31d99ff880
0x31d99ff884
0x31d99ff884

而这种操作在Go中是不允许的。

拓展

在后面我会介绍标准库unsafe标准库,可以一定程度上实现指针偏移,大家有兴趣也可以结合官方文档进行学习

new与make

在前面的几节已经很多次提到过内置函数newmake,两者有点类似,但也有不同,下面复习下。

func new(Type) *Type
  • 返回值是类型指针
  • 接收参数是类型
  • 专用于给指针分配内存空间
func make(t Type, size ...IntegerType) Type
  • 返回值是值,不是指针
  • 接收的第一个参数是类型,不定长参数根据传入类型的不同而不同
  • 专用于给切片,映射表,通道分配内存。

结构体

前言

Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。结构体可以存储一组不同类型的数据,是一种复合类型,下面是一个示例:

type builder struct {
  name string 
  age int 
  sex string
}

结构体的声明

结构体的声明非常简单,例子如下:

type Person struct {
   name string
   age int
}

结构体本身以及其内部的字段都遵守大小写命名的暴露方式。对于一些类型相同的字段,可以像如下方式声明:

type Rectangle struct {
  height, width, area int
  color               string
}

结构体的创建

Go不存在构造方法,大多数情况下采用如下的方式来创建。

programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

初始化的时候就像map一样指定字段名称再初始化字段值,不过也可以省略字段名称。

programmer := Programmer{
   "jack",
   19,
   "coder",
   []string{"Go", "C++"}}

当省略字段名称时,就必须初始化所有字段,且必须按照声明的顺序初始化。

func NewProgrammer() Programmer {
   return Programmer{
      "jack",
      19,
      "coder",
      []string{"Go", "C++"}}
}

补充:其实我们也可以专门编写一个代码来初始化结构体,子类函数我们一般会叫它工厂方法,这也是其为什么没有构造方法的原因之一,后面在下一篇文章中我在详细介绍。

结构体的组合

前言

我们知道,Go本身其实刻意的弱化了面向对象功能并且抛弃了类与继承,但是我们还是要有办法表示结构体与结构体之间的关系,这时候我们就要使用组合来模拟出类似于继承的效果

组合实现的两种方式

  • 显式组合
type Person struct {
   name string
   age  int
}
type Student struct {
   p      Person
   school string
}
type Employee struct {
   p   Person
   job string
}
  • 而我们在使用时也需要显式的指定字段p
student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)
  • 匿名组合
    匿名组合可以不用显式的指定字段
type Person struct {
  name string
  age  int
}
type Student struct {
  Person
  school string
}
type Employee struct {
  Person
  job string
}
  • 匿名字段的名称默认为类型名,调用者可以直接访问该类型的字段和方法,但除了更加方便以外与第一种方式没有任何的区别。
student := Student{
   Person: Person{name: "jack",age: 18},
   school: "lili school",
}
fmt.Println(student.name)

结构体指针

对于结构体指针而言,不需要解引用就可以直接访问结构体的内容,例子如下:

p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age,p.name)

在编译的时候会转换为(*p).name(*p).age,其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖。

空结构体

空结构体没有字段,不占用内存空间,可以通过unsafe.SizeOf函数来计算占用的字节大小

func main() {
   type Empty struct {
      
   }
   fmt.Println(unsafe.Sizeof(Empty{}))
}

输出

0

空结构体的使用场景有很多,比如之前提到过的,作为map的值类型,可以将map作为set来进行使用,又或者是作为通道的类型,即代表一个不发送数据的通道。

结语

在上面我们介绍了数组与切片,字符串,映射表,指针以及结构体等常见的基本数据结构,由于这篇文章主要是一些基础性的知识,所以有关slice,map的具体实现原理,我并没有去展开讲,这一部分我会在介绍一些底层原理如Go的垃圾回收机制时一并奉上,由于博主也是刚刚开始学习Go语言,有些东西表达可能并不清楚,也欢迎大家斧正,下篇文章我们将要进入Go语言面向对象编程的部分,虽然Go语言淡化了面向对象,但是面向对象的抽象,封装,继承与多态依旧是我们书写代码时上面值得借鉴的思想,所以下一篇我将尝试利用Go语言来实现面向对象,同时我也会基于牛客写一些关于Go语言的算法题题解,欢迎大家指正。

相关文章
|
19天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
31 7
|
18天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
19天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
93 71
|
18天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
100 67
|
21天前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
91 62
|
23天前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
19天前
|
存储 Go
go语言中映射
go语言中映射
32 11
|
21天前
|
Go
go语言for遍历映射(map)
go语言for遍历映射(map)
31 12
|
20天前
|
Go 索引
go语言使用索引遍历
go语言使用索引遍历
28 9
|
24天前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####