一文弄懂:【Go】内存中的结构体

开发 前端
在数据存储上来讲,结构体和数组没有太大的区别. 只不过结构体的各个字段(元素)类型可以相同,也可以不同,所以只能通过字段的相对偏移量进行访问.

[[441160]]

结构体

所谓结构体,实际上就是由各种类型的数据组合而成的一种复合数据类型.

在数据存储上来讲,结构体和数组没有太大的区别. 只不过结构体的各个字段(元素)类型可以相同,也可以不同,所以只能通过字段的相对偏移量进行访问. 而数组的各个元素类型相同,可以通过索引快速访问,实际其本质上也是通过相对偏移量计算地址进行访问.

因为结构体的各个字段类型不同,有大有小,而结构体在存储时通常需要进行内存对齐,所以结构体在存储时可能会出现"空洞",也就是无法使用到的内存空间.

在之前的Go系列文章中,我们接触最多的结构体是reflect包中的rtype,可以说已经非常熟悉.

  1. type rtype struct { 
  2.     size       uintptr 
  3.     ptrdata    uintptr // number of bytes in the type that can contain pointers 
  4.     hash       uint32  // hash of type; avoids computation in hash tables 
  5.     tflag      tflag   // extra type information flags 
  6.     align      uint8   // alignment of variable with this type 
  7.     fieldAlign uint8   // alignment of struct field with this type 
  8.     kind       uint8   // enumeration for C 
  9.     equal      func(unsafe.Pointer, unsafe.Pointer) bool 
  10.     gcdata     *byte   // garbage collection data 
  11.     str        nameOff // string form 
  12.     ptrToThis  typeOff // type for pointer to this type, may be zero 

在64位程序和系统中占48个字节,其结构分布如下:

在Go语言中,使用reflect.rtype结构体描述任何Go类型的基本信息.

在Go语言中,使用reflect.structType结构体描述结构体类别(reflect.Struct)数据的类型信息,定义如下:

  1. // structType represents a struct type. 
  2. type structType struct { 
  3.     rtype 
  4.     pkgPath name 
  5.     fields  []structField // sorted by offset 
  6.  
  7. // Struct field 
  8. type structField struct { 
  9.     name        name    // name is always non-empty 
  10.     typ         *rtype  // type of field 
  11.     offsetEmbed uintptr // byte offset of field<<1 | isEmbedded 

在64位程序和系统中占80个字节,其结构分布如下:

在之前的几篇文章中,已经详细介绍了类型方法相关内容,如果还未阅读,建议不要错过:

  • 再谈整数类型
  • 深入理解函数
  • 内存中的接口类型

在Go语言中,结构体类型不但可以包含字段,还可以定义方法,实际上完整的类型信息结构分布如下:

当然,结构体是可以不包含字段的,也可以没有方法的.

环境

  1. OS : Ubuntu 20.04.2 LTS; x86_64 
  2. Go : go version go1.16.2 linux/amd64 

声明

操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异。

本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析。

本文仅保证学习过程中的分析数据在当前环境下的准确有效性。

代码清单

在Go语言中,结构体随处可见,所以本文示例代码中不再自定义结构体,而是使用Go语言中常用的结构体用于演示.

在 命令行参数详解 一文中,曾详细介绍过flag.FlagSet结构体.

本文,我们将详细介绍flag.FlagSet和reflect.Value两个结构体的类型信息.

  1. package main 
  2.  
  3. import ( 
  4.   "flag" 
  5.   "fmt" 
  6.   "reflect" 
  7.  
  8. func main() { 
  9.   f := flag.FlagSet{} 
  10.  
  11.   Print(reflect.TypeOf(f)) 
  12.   Print(reflect.TypeOf(&f)) 
  13.  
  14.   _ = f.Set("hello""world"
  15.   f.PrintDefaults() 
  16.   fmt.Println(f.Args()) 
  17.  
  18.   v := reflect.ValueOf(f) 
  19.   Print(reflect.TypeOf(v)) 
  20.   Print(reflect.TypeOf(&v)) 
  21.  
  22.   Print(reflect.TypeOf(struct{}{})) 
  23.  
  24. //go:noinline 
  25. func Print(t reflect.Type) { 
  26.   fmt.Printf("Type = %s\t, address = %p\n", t, t) 

运行

从运行结果可以看到:

  • 结构体flag.FlagSet的类型信息保存在0x4c2ac0地址处.
  • 结构体指针*flag.FlagSet的类型信息保存在0x4c68e0地址处.
  • 结构体reflect.Value的类型信息保存在0x4ca160地址处.
  • 结构体指针*reflect.Value的类型信息保存在0x4c9c60地址处.
  • 匿名结构体struct{}{}的类型信息保存在0x4b4140地址处.

内存分析

在main函数入口处设置断点进行调试.我们先从简单的结构体开始分析.

匿名结构体struct{}

该结构体既没有字段,也没有方法,其类型信息数据如下:

  • rtype.size = 0x0 (0)
  • rtype.ptrdata = 0x0 (0)
  • rtype.hash = 0x27f6ac1b
  • rtype.tflag = tflagExtraStar | tflagRegularMemory
  • rtype.align = 1
  • rtype.fieldAlign = 1
  • rtype.kind = 0x19 (25) -> reflect.Struct
  • rtype.equal = 0x4d3100 -> runtime.memequal0
  • rtype.gcdata = 0x4ea04f
  • rtype.str = 0x0000241f -> "struct {}"
  • rtype.ptrToThis = 0x0 (0x0)
  • structType.pkgPath = 0 -> ""
  • structType.fields = []

这是一个特殊的结构体,没有字段,没有方法,不占用内存空间,明明定义在main包中,但是包路径信息为空,存储结构分布如下:

好神奇的是,struct{}类型的对象居然是可以比较的,其比较函数是runtime.memequal0,定义如下:

  1. func memequal0(p, q unsafe.Pointer) bool { 
  2.     return true 

也就是说,所有的struct{}类型的对象,无论它们在内存的什么位置,无论它们是在什么时间创建的,永远都是相等的.

细细品,还是蛮有道理的.

结构体类型flag.FlagSet

结构体flag.FlagSet包含8个字段,其类型信息占用288个字节.

  • rtype.size = 0x60 (96)
  • rtype.ptrdata = 0x60 (96)
  • rtype.hash = 0x644236d1
  • rtype.tflag = tflagUncommon | tflagExtraStar | tflagNamed
  • rtype.align = 8
  • rtype.fieldAlign = 8
  • rtype.kind = 0x19 (25) -> reflect.Struct
  • rtype.equal = nil
  • rtype.gcdata = 0x4e852c
  • rtype.str = 0x32b0 -> "flag.FlagSet"
  • rtype.ptrToThis = 0x208e0 (0x4c68e0)
  • structType.pkgPath = 0x4a6368 -> "flag"
  • structType.fields.Data = 0x4c2b20
  • structType.fields.Len = 8 -> 字段数量
  • structType.fields.Cap = 8
  • uncommonType.pkgpath = 0x368 -> "flag"
  • uncommonType.mcount = 0 -> 方法数量
  • uncommonType.xcount = 0
  • uncommonType.moff = 208
  • structType.fields =
  1. [   
  2.   { 
  3.       name        = 0x4a69a0 -> Usage 
  4.       typ         = 0x4b0140 -> func() 
  5.       offsetEmbed = 0x0 (0) 
  6.   },   
  7.   { 
  8.       name        = 0x4a69a0 -> name 
  9.       typ         = 0x4b1220 -> string 
  10.       offsetEmbed = 0x8 (8) 
  11.   },   
  12.   { 
  13.       name        = 0x4a704a -> parsed 
  14.       typ         = 0x4b0460 -> bool 
  15.       offsetEmbed = 0x18 (24) 
  16.   },   
  17.   { 
  18.       name        = 0x4a6e64 -> actual 
  19.       typ         = 0x4b4c20 -> map[string]*flag.Flag 
  20.       offsetEmbed = 0x20 (32) 
  21.   },   
  22.   { 
  23.       name        = 0x4a6f0f -> formal 
  24.       typ         = 0x4b4c20 -> map[string]*flag.Flag 
  25.       offsetEmbed = 0x28 (40) 
  26.   },   
  27.   { 
  28.       name        = 0x4a646d -> args 
  29.       typ         = 0x4afe00 -> []string 
  30.       offsetEmbed = 0x30 (48) 
  31.   },   
  32.   { 
  33.       name        = 0x4a9450 -> errorHandling 
  34.       typ         = 0x4b05a0 -> flag.ErrorHandling 
  35.       offsetEmbed = 0x48 (72) 
  36.   },   
  37.   { 
  38.       name        = 0x4a702f -> output 
  39.       typ         = 0x4b65c0 -> io.Writer 
  40.       offsetEmbed = 0x50 (80) 
  41.   } 

从以上数据可以看到,结构体flag.FlagSet类型的数据对象,占用96字节的存储空间,并且所有字段全部被视为指针数据.

flag.FlagSet类型的对象不可比较,因为其rtype.equal字段值nil. 除了struct{}这个特殊的结构体类型,估计是不容易找到可比较的结构体类型了.

从以上字段数据可以看到,FlagSet.parsed字段的偏移量是24,FlagSet.actual字段的偏移量是32;也就是说,bool类型的FlagSet.parsed字段实际占用8字节的存储空间.

bool类型的实际值只能是0或1,只需要占用一个字节即可,实际的机器指令也会读取一个字节. 也就是,flag.FlagSet类型的对象在存储时,因为8字节对齐,此处需要浪费7个字节的空间.

从以上字段数据可以看到,string类型的字段占16个字节,[]string类型的字段占24个字节,接口类型的字段占16个字节,与之前文章中分析得到的结果一直.

另外,可以看到map类型的字段,实际占用8个字节的空间,在之后的文章中将会详细介绍map类型.

仔细的读者可能已经注意到,flag.FlagSet类型没有任何方法,因为其uncommonType.mcount = 0.

在flag/flag.go源文件中,不是定义了很多方法吗?

以上代码清单中,flag.FlagSet类型的对象f为什么可以调用以下方法呢?

  1. _ = f.Set("hello""world"
  2.   f.PrintDefaults() 
  3.   fmt.Println(f.Args()) 

实际上,flag/flag.go源文件中定义的方法的receiver都是*flag.FlagSet指针类型,没有flag.FlagSet类型.

  1. // Args returns the non-flag arguments. 
  2. func (f *FlagSet) Args() []string { return f.args } 

flag.FlagSet类型的对象f能够调用*flag.FlagSet指针类型的方法,只不过是编译器为方便开发者实现的语法糖而已.

在本例中,编译器会把flag.FlagSet类型的对象f的地址作为参数传递给*flag.FlagSet指针类型的方法.反之,编译器也是支持的.

指针类型*flag.FlagSet

为了方便查看类型信息,笔者开发了一个gdb的插件脚本.

查看*flag.FlagSet类型的信息如下,共包含38个方法,其中34个是公共方法.此处不再一一介绍.

  1. (gdb) info type 0x4c68e0 
  2. interfaceType { 
  3.   rtype = { 
  4.     size       = 0x8 (8) 
  5.     ptrdata    = 0x8 (8) 
  6.     hash       = 0xe05aa02c 
  7.     tflag      = tflagUncommon | tflagRegularMemory 
  8.     align      = 8 
  9.     fieldAlign = 8 
  10.     kind       = ptr 
  11.     equal      = 0x403a00 <runtime.memequal64> 
  12.     gcdata     = 0x4d2e28 
  13.     str        = *flag.FlagSet 
  14.     ptrToThis  = 0x0 (0x0) 
  15.   } 
  16.   elem  = 0x4c2ac0 -> flag.FlagSet 
  17. uncommonType { 
  18.   pkgpath = flag 
  19.   mcount  = 38 
  20.   xcount  = 34 
  21.   moff    = 16 
  22. methods [ 
  23.   { 
  24.     name = Arg 
  25.     mtyp = nil 
  26.     ifn  = nil 
  27.     tfn  = nil 
  28.   }, 
  29.   { 
  30.     name = Args 
  31.     mtyp = nil 
  32.     ifn  = nil 
  33.     tfn  = nil 
  34.   }, 
  35.   { 
  36.     name = Bool 
  37.     mtyp = nil 
  38.     ifn  = nil 
  39.     tfn  = nil 
  40.   }, 
  41.   { 
  42.     name = BoolVar 
  43.     mtyp = nil 
  44.     ifn  = nil 
  45.     tfn  = nil 
  46.   }, 
  47.   { 
  48.     name = Duration 
  49.     mtyp = nil 
  50.     ifn  = nil 
  51.     tfn  = nil 
  52.   }, 
  53.   { 
  54.     name = DurationVar 
  55.     mtyp = nil 
  56.     ifn  = nil 
  57.     tfn  = nil 
  58.   }, 
  59.   { 
  60.     name = ErrorHandling 
  61.     mtyp = nil 
  62.     ifn  = nil 
  63.     tfn  = nil 
  64.   }, 
  65.   { 
  66.     name = Float64 
  67.     mtyp = nil 
  68.     ifn  = nil 
  69.     tfn  = nil 
  70.   }, 
  71.   { 
  72.     name = Float64Var 
  73.     mtyp = nil 
  74.     ifn  = nil 
  75.     tfn  = nil 
  76.   }, 
  77.   { 
  78.     name = Func 
  79.     mtyp = nil 
  80.     ifn  = nil 
  81.     tfn  = nil 
  82.   }, 
  83.   { 
  84.     name = Init 
  85.     mtyp = nil 
  86.     ifn  = nil 
  87.     tfn  = nil 
  88.   }, 
  89.   { 
  90.     name = Int 
  91.     mtyp = nil 
  92.     ifn  = nil 
  93.     tfn  = nil 
  94.   }, 
  95.   { 
  96.     name = Int64 
  97.     mtyp = nil 
  98.     ifn  = nil 
  99.     tfn  = nil 
  100.   }, 
  101.   { 
  102.     name = Int64Var 
  103.     mtyp = nil 
  104.     ifn  = nil 
  105.     tfn  = nil 
  106.   }, 
  107.   { 
  108.     name = IntVar 
  109.     mtyp = nil 
  110.     ifn  = nil 
  111.     tfn  = nil 
  112.   }, 
  113.   { 
  114.     name = Lookup 
  115.     mtyp = nil 
  116.     ifn  = nil 
  117.     tfn  = nil 
  118.   }, 
  119.   { 
  120.     name = NArg 
  121.     mtyp = 0x4b0960 -> func() int 
  122.     ifn  = nil 
  123.     tfn  = nil 
  124.   }, 
  125.   { 
  126.     name = NFlag 
  127.     mtyp = 0x4b0960 -> func() int 
  128.     ifn  = nil 
  129.     tfn  = nil 
  130.   }, 
  131.   { 
  132.     name = Name 
  133.     mtyp = 0x4b0b20 -> func() string 
  134.     ifn  = 0x4a36e0 <flag.(*FlagSet).Name
  135.     tfn  = 0x4a36e0 <flag.(*FlagSet).Name
  136.   }, 
  137.   { 
  138.     name = Output 
  139.     mtyp = nil 
  140.     ifn  = nil 
  141.     tfn  = nil 
  142.   }, 
  143.   { 
  144.     name = Parse 
  145.     mtyp = nil 
  146.     ifn  = nil 
  147.     tfn  = nil 
  148.   }, 
  149.   { 
  150.     name = Parsed 
  151.     mtyp = 0x4b0920 -> func() bool 
  152.     ifn  = nil 
  153.     tfn  = nil 
  154.   }, 
  155.   { 
  156.     name = PrintDefaults 
  157.     mtyp = 0x4b0140 -> func() 
  158.     ifn  = 0x4a3ec0 <flag.(*FlagSet).PrintDefaults> 
  159.     tfn  = 0x4a3ec0 <flag.(*FlagSet).PrintDefaults> 
  160.   }, 
  161.   { 
  162.     name = Set 
  163.     mtyp = nil 
  164.     ifn  = 0x4a37a0 <flag.(*FlagSet).Set
  165.     tfn  = 0x4a37a0 <flag.(*FlagSet).Set
  166.   }, 
  167.   { 
  168.     name = SetOutput 
  169.     mtyp = nil 
  170.     ifn  = nil 
  171.     tfn  = nil 
  172.   }, 
  173.   { 
  174.     name = String 
  175.     mtyp = nil 
  176.     ifn  = nil 
  177.     tfn  = nil 
  178.   }, 
  179.   { 
  180.     name = StringVar 
  181.     mtyp = nil 
  182.     ifn  = nil 
  183.     tfn  = nil 
  184.   }, 
  185.   { 
  186.     name = Uint 
  187.     mtyp = nil 
  188.     ifn  = nil 
  189.     tfn  = nil 
  190.   }, 
  191.   { 
  192.     name = Uint64 
  193.     mtyp = nil 
  194.     ifn  = nil 
  195.     tfn  = nil 
  196.   }, 
  197.   { 
  198.     name = Uint64Var 
  199.     mtyp = nil 
  200.     ifn  = nil 
  201.     tfn  = nil 
  202.   }, 
  203.   { 
  204.     name = UintVar 
  205.     mtyp = nil 
  206.     ifn  = nil 
  207.     tfn  = nil 
  208.   }, 
  209.   { 
  210.     name = Var 
  211.     mtyp = nil 
  212.     ifn  = nil 
  213.     tfn  = nil 
  214.   }, 
  215.   { 
  216.     name = Visit 
  217.     mtyp = nil 
  218.     ifn  = nil 
  219.     tfn  = nil 
  220.   }, 
  221.   { 
  222.     name = VisitAll 
  223.     mtyp = nil 
  224.     ifn  = 0x4a3700 <flag.(*FlagSet).VisitAll> 
  225.     tfn  = 0x4a3700 <flag.(*FlagSet).VisitAll> 
  226.   }, 
  227.   { 
  228.     name = defaultUsage 
  229.     mtyp = 0x4b0140 -> func() 
  230.     ifn  = 0x4a3f20 <flag.(*FlagSet).defaultUsage> 
  231.     tfn  = 0x4a3f20 <flag.(*FlagSet).defaultUsage> 
  232.   }, 
  233.   { 
  234.     name = failf 
  235.     mtyp = nil 
  236.     ifn  = nil 
  237.     tfn  = nil 
  238.   }, 
  239.   { 
  240.     name = parseOne 
  241.     mtyp = nil 
  242.     ifn  = nil 
  243.     tfn  = nil 
  244.   }, 
  245.   { 
  246.     name = usage 
  247.     mtyp = 0x4b0140 -> func() 
  248.     ifn  = nil 
  249.     tfn  = nil 
  250.   } 

结构体类型reflect.Value

实际上,编译器比想象的做的更多.

有时候,编译器会把源代码中的一个方法,编译出两个可执行的方法.在 内存中的接口类型 一文中,曾进行了详细分析.

直接运行gdb脚本查看reflect.Value类型信息,有3个字段,75个方法,此处为方便展示,省略了大部分方法信息.

  1. (gdb) info type 0x4ca160 
  2. structType { 
  3.   rtype   = { 
  4.     size       = 0x18 (24) 
  5.     ptrdata    = 0x10 (16) 
  6.     hash       = 0x500c1abc 
  7.     tflag      = tflagUncommon | tflagExtraStar | tflagNamed | tflagRegularMemory 
  8.     align      = 8 
  9.     fieldAlign = 8 
  10.     kind       = struct 
  11.     equal      = 0x402720 <runtime.memequal_varlen> 
  12.     gcdata     = 0x4d2e48 
  13.     str        = reflect.Value 
  14.     ptrToThis  = 0x23c60 (0x4c9c60) 
  15.   } 
  16.   pkgPath = reflect 
  17.   fields  = [   
  18.     { 
  19.       name        = 0x4875094 -> typ 
  20.       typ         = 0x4c6e60 -> *reflect.rtype 
  21.       offsetEmbed = 0x0 (0)  
  22.     },   
  23.     { 
  24.       name        = 0x4874896 -> ptr 
  25.       typ         = 0x4b13e0 -> unsafe.Pointer 
  26.       offsetEmbed = 0x8 (8)  
  27.     },   
  28.     { 
  29.       name        = 0x4875112 -> flag 
  30.       typ         = 0x4be7c0 -> reflect.flag 
  31.       offsetEmbed = 0x10 (16) embed 
  32.     } 
  33.   ] 
  34. uncommonType { 
  35.   pkgpath = reflect 
  36.   mcount  = 75 
  37.   xcount  = 61 
  38.   moff    = 88 
  39. methods [ 
  40.   { 
  41.     name = Addr 
  42.     mtyp = nil 
  43.     ifn  = nil 
  44.     tfn  = nil 
  45.   }, 
  46.   { 
  47.     name = Bool 
  48.     mtyp = 0x4b0920 -> func() bool 
  49.     ifn  = nil 
  50.     tfn  = 0x4881c0 <reflect.Value.Bool> 
  51.   }, 
  52.   ...... 
  53.   { 
  54.     name = Kind 
  55.     mtyp = 0x4b0aa0 -> func() reflect.Kind 
  56.     ifn  = 0x48d500 <reflect.(*Value).Kind> 
  57.     tfn  = 0x489400 <reflect.Value.Kind> 
  58.   }, 
  59.   { 
  60.     name = Len 
  61.     mtyp = 0x4b0960 -> func() int 
  62.     ifn  = 0x48d560 <reflect.(*Value).Len> 
  63.     tfn  = 0x489420 <reflect.Value.Len> 
  64.   }, 
  65.   ...... 

再看*reflect.Value指针类型的信息,没有任何字段(毕竟是指针),也有75个方法.

  1. (gdb) info type 0x4c9c60 
  2. interfaceType { 
  3.   rtype = { 
  4.     size       = 0x8 (8) 
  5.     ptrdata    = 0x8 (8) 
  6.     hash       = 0xf764ad0 
  7.     tflag      = tflagUncommon | tflagRegularMemory 
  8.     align      = 8 
  9.     fieldAlign = 8 
  10.     kind       = ptr 
  11.     equal      = 0x403a00 <runtime.memequal64> 
  12.     gcdata     = 0x4d2e28 
  13.     str        = *reflect.Value 
  14.     ptrToThis  = 0x0 (0x0) 
  15.   } 
  16.   elem  = 0x4ca160 -> reflect.Value 
  17. uncommonType { 
  18.   pkgpath = reflect 
  19.   mcount  = 75 
  20.   xcount  = 61 
  21.   moff    = 16 
  22. methods [ 
  23.   { 
  24.     name = Addr 
  25.     mtyp = nil 
  26.     ifn  = nil 
  27.     tfn  = nil 
  28.   }, 
  29.   { 
  30.     name = Bool 
  31.     mtyp = 0x4b0920 -> func() bool 
  32.     ifn  = nil 
  33.     tfn  = nil 
  34.   }, 
  35.   ...... 
  36.   { 
  37.     name = Kind 
  38.     mtyp = 0x4b0aa0 -> func() reflect.Kind 
  39.     ifn  = 0x48d500 <reflect.(*Value).Kind> 
  40.     tfn  = 0x48d500 <reflect.(*Value).Kind> 
  41.   }, 
  42.   { 
  43.     name = Len 
  44.     mtyp = 0x4b0960 -> func() int 
  45.     ifn  = 0x48d560 <reflect.(*Value).Len> 
  46.     tfn  = 0x48d560 <reflect.(*Value).Len> 
  47.   }, 
  48.   ...... 

我们可以清楚地看到,在源码中Len()方法,编译之后,生成了两个可执行方法,分别是:

  • reflect.Value.Len
  • reflect.(*Value).Len
  1. func (v Value) Len() int { 
  2.   k := v.kind() 
  3.   switch k { 
  4.   case Array: 
  5.     tt := (*arrayType)(unsafe.Pointer(v.typ)) 
  6.     return int(tt.len) 
  7.   case Chan: 
  8.     return chanlen(v.pointer()) 
  9.   case Map: 
  10.     return maplen(v.pointer()) 
  11.   case Slice: 
  12.     // Slice is bigger than a word; assume flagIndir. 
  13.     return (*unsafeheader.Slice)(v.ptr).Len 
  14.   case String: 
  15.     // String is bigger than a word; assume flagIndir. 
  16.     return (*unsafeheader.String)(v.ptr).Len 
  17.   } 
  18.   panic(&ValueError{"reflect.Value.Len", v.kind()}) 

通过reflect.Value类型的对象调用时,实际可能执行的两个方法中的任何一个.

通过*reflect.Value类型的指针对象调用时,也可能执行的两个方法中的任何一个.

这完全是由编译器决定的.

但是通过接口调用时,执行的一定是reflect.(*Value).Len这个方法的指令集合.

自定义结构体千变万化,但是结构体类型信息相对还是单一,容易理解.

 

责任编辑:姜华 来源: Golang In Memory
相关推荐

2023-11-21 08:03:43

语言架构偏移量

2022-08-09 09:10:43

Kubernetes容器

2023-11-28 09:31:55

MySQL算法

2023-03-30 08:52:40

DartFlutter

2021-06-02 05:43:36

比特币虚拟货币区块链

2023-03-27 17:58:34

MySQL加锁间隙锁

2022-08-03 08:01:16

CDN网站服务器

2023-09-18 08:02:45

CSS布局属性

2023-10-26 16:27:50

前端 WebCSS开发

2024-05-09 10:11:30

2023-12-12 07:31:51

Executors工具开发者

2022-09-01 08:01:56

Pythongunicorn

2023-04-04 08:01:47

2022-09-05 09:25:53

KubernetesService

2022-01-04 08:54:32

Redis数据库数据类型

2022-09-09 10:00:13

KubernetesConfigMap

2024-10-16 10:11:52

2019-09-27 08:53:47

Redis数据C语言

2020-01-14 12:08:32

内存安全

2024-02-23 19:11:13

C++编程开发
点赞
收藏

51CTO技术栈公众号