Go 语言中的程序实体包括:变量、常量、函数、结构体和接口。
Go 语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,这样才可以让 Go 语言能够推导出它们的类型,在 Go 语言中,变量的类型可以是其预定义的那些类型,也可以是程序自定义的函数、结构体或接口。常量的合法类型不多,只能是那些 Go 语言预定义的基本类型。它的声明方式也更简单一些。
网站建设哪家好,找成都创新互联公司!专注于网页设计、网站建设、微信开发、小程序开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了城区免费建站欢迎大家使用!
package main
import (
"flag"
"fmt"
)
func main() {
var name string //var name string这种声明变量name的方式 // [1]
flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
// 方式1。
//var name = flag.String("name", "everyone", "The greeting object.")
// 方式2。
//name := flag.String("name", "everyone", "The greeting object.")
flag.Parse()
fmt.Printf("Hello, %v!\n", name)
// 适用于方式1和方式2。
//fmt.Printf("Hello, %v!\n", *name)
}
第一种方式中的代码在声明变量name的同时,还为它赋了值,而这时声明中并没有显式指定name的类型。这里利用了 Go 语言自身的类型推断,而省去了对该变量的类型的声明。
把被调用的函数由flag.StringVar改为flag.String,传参的列表也需要随之修改,这是为了[1]和[2]处代码合并的准备工作。
注意,flag.String函数返回的结果值的类型是string而不是string。类型string代表的是字符串的指针类型,而不是字符串类型。因此,这里的变量name代表的是一个指向字符串值的指针。
我们可以通过操作符把这个指针指向的字符串值取出来了。因此,在这种情况下,那个被用来打印内容的函数调用就需要微调一下,把其中的参数name改为name,即:fmt.Printf("Hello, %v!\n", *name)。
第二种方式与第一种方式非常类似,它基于第一种方式的代码,赋值符号=右边的代码不动,左边只留下name,再把=变成:=
var name = flag.String("name", "everyone", "The greeting object.")
第一种方式中的代码在声明变量name的同时,还为它赋了值,而这时声明中并没有显式指定name的类型。
这里利用了 Go 语言自身的类型推断,而省去了对该变量的类型的声明。
简单地说,类型推断是一种编程语言在编译期自动解释表达式类型的能力。什么是表达式?详细的解释你可以参看 Go 语言规范中的表达式https://golang.google.cn/ref/spec#Expressions 和表达式语句https://golang.google.cn/ref/spec#Expression_statements 章节
表达式类型就是对表达式进行求值后得到结果的类型。Go 语言中的类型推断是很简约的,这也是 Go 语言整体的风格。
它只能用于对变量或常量的初始化,就像上述回答中描述的那样。对flag.String函数的调用其实就是一个调用表达式,而这个表达式的类型是*string,即字符串的指针类型。
这也是调用flag.String函数后得到结果的类型。随后,Go 语言把这个调用了flag.String函数的表达式类型,直接作为了变量name的类型,这就是“推断”一词所指代的操作了。
name := flag.String("name", "everyone", "The greeting object.")
至于第二种方式所用的短变量声明,实际上就是 Go 语言的类型推断再加上一点点语法糖。
我们只能在函数体内部使用短变量声明
。在编写if、for或switch语句的时候,我们经常把它安插在初始化子句中,并用来声明一些临时的变量。而相比之下,第一种方式更加通用,它可以被用在任何地方。
先看一段代码:
package main
import (
"flag"
"fmt"
)
func main() {
var name = getTheFlag()
flag.Parse()
fmt.Printf("Hello, %v!\n", *name)
}
func getTheFlag() *string {
return flag.String("name", "everyone", "The greeting object.")
}
//上面函数的实现也可以是这样的。
//func getTheFlag() *int {
// return flag.Int("num", 1, "The number of greeting object.")
//}
go run demo8.go -name huaihe
Hello, huaihe!
name能不能是数字呢?
package main
import (
"flag"
"fmt"
)
func main() {
var name = getTheFlag()
flag.Parse()
fmt.Printf("Hello, %v!\n", *name)
}
// func getTheFlag() *string {
// return flag.String("name", "everyone", "The greeting object.")
// }
//上面函数的实现也可以是这样的。
func getTheFlag() *int {
return flag.Int("name", 1, "The number of greeting object.")
}
name输出已经是一个数字了
go run demo8.go -name=2
Hello, 2!
我们可以用getTheFlag函数包裹(或者说包装)那个对flag.String函数的调用,并把其结果直接作为getTheFlag函数的结果,结果的类型是*string。
这样一来,var name =右边的表达式,可以变为针对getTheFlag函数的调用表达式了。这实际上是对“声明并赋值name变量的那行代码”的重构。
通常把不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫做对该程序的重构。重构的对象可以是一行代码、一个函数、一个功能模块,甚至一个软件系统。
好了,在准备工作做完之后,你会发现,你可以随意改变getTheFlag函数的内部实现,及其返回结果的类型,而不用修改main函数中的任何代码。
这个命令源码文件依然可以通过编译,并且构建和运行也都不会有问题。也许你能感觉得到,这是一个关于程序灵活性的质变。
我们不显式地指定变量name的类型,使得它可以被赋予任何类型的值。也就是说,变量name的类型可以在其初始化时,由其他程序动态地确定。
在你改变getTheFlag函数的结果类型之后,Go 语言的编译器会在你再次构建该程序的时候,自动地更新变量name的类型。
通过这种类型推断,你可以体验到动态类型编程语言所带来的一部分优势,即程序灵活性的明显提升。但在那些编程语言中,这种提升可以说是用程序的可维护性和运行效率换来的。
Go 语言是静态类型的,所以一旦在初始化变量时确定了它的类型,之后就不可能再改变。这就避免了在后面维护程序时的一些问题。另外,请记住,这种类型的确定是在编译期完成的,因此不会对程序的运行效率产生任何影响。
总结:
Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率。
变量声明。通过使用它,我们可以对同一个代码块中的变量进行重声明。
说到了代码块,我先来解释一下它。在 Go 语言中,代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句。Go 语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫全域代码块。
这主要体现在,只要是公开的全局变量,都可以被任何代码所使用。相对小一些的代码块是代码包,一个代码包可以包含许多子代码包,所以这样的代码块也可以很大。
接下来,每个源码文件也都是一个代码块,每个函数也是一个代码块,每个if语句、for语句、switch语句和select语句都是一个代码块。甚至,switch或select语句中的case子句也都是独立的代码块。走个极端,我就在main函数中写一对紧挨着的花括号算不算一个代码块?当然也算,这甚至还有个名词,叫“空代码块”。
变量重声明的前提条件如下:
变量重声明其实算是一个语法糖(或者叫便利措施)。它允许我们在使用短变量声明时不用理会被赋值的多个变量中是否包含旧变量。可以想象,如果不这样会多写不少代码。
package main
import (
"fmt"
"io"
"os"
)
func main() {
var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n") // 这里对`err`进行了重声明。
if err != nil {
fmt.Printf("Error: %v\n", err)
}
fmt.Printf("%d byte(s) were written.\n", n)
}
使用短变量声明对新变量n和旧变量err进行了“声明并赋值”,这时也是对后者的重声明。
在本篇中,我们聚焦于最基本的 Go 语言程序实体:变量。并详细解说了变量声明和赋值的基本方法,及其背后的重要概念和知识。我们使用关键字var和短变量声明,都可以实现对变量的“声明并赋值”。
这两种方式各有千秋,有着各自的特点和适用场景。前者可以被用在任何地方,而后者只能被用在函数或者其他更小的代码块中。
不过,通过前者我们无法对已有的变量进行重声明,也就是说它无法处理新旧变量混在一起的情况。不过它们也有一个很重要的共同点,即:基于类型推断,Go 语言的类型推断只应用在了对变量或常量的初始化方面。
package main
import "fmt"
var block = "package"
func main() {
block := "function"
{
block := "inner"
fmt.Printf("The block is %s.\n", block)
}
fmt.Printf("The block is %s.\n", block)
}
执行结果:
go run demo10.go
The block is inner.
The block is function.
程序实体的访问权限有三种:包级私有的、模块级私有的和公开的,包级私有和模块级私有访问权限对应的都是代码包代码块,公开的访问权限对应的是全域代码块。
这个命令源码文件中有四个代码块,它们是:全域代码块、main包代表的代码块、main函数代表的代码块,以及在main函数中的一个用花括号包起来的代码块。后三个代码块中分别声明了一个名为block的变量,并分别把字符串值"package"、"function"和"inner"赋给了它们。此外,我在后两个代码块的最后分别尝试用fmt.Printf函数打印出“The block is %s.”。这里的“%s”只是为了占位,程序会用block变量的实际值替换掉。
首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量。注意,这里的“当前代码块”仅仅是引用变量的代码所在的那个代码块,并不包含任何子代码块。
其次,如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始,一层一层地查找。
一般情况下,程序会一直查到当前代码包代表的代码块。如果仍然找不到,那么 Go 语言的编译器就会报错了。
从作用域的角度也可以说,虽然通过var block = "package"声明的变量作用域是整个main代码包,但是在main函数中,它却被那两个同名的变量“屏蔽”了。
虽然main函数首先声明的block的作用域,是整个main函数,但是在最内层的那个代码块中,它却是不可能被引用到的。
最内层的{ }代码块会使用当前代码块{ }的变量block := "inner",所以第一次打印The block is inner.。
最内层代码块中的block也不可能被该块之外的main代码引用到,所以第二行打印“The block is function.”
方便描述,把不同代码块中的重名变量叫做“可重名变量”。注意,在同一个代码块中不允许出现重名的变量,这违背了 Go 语言的语法。
(1)变量重声明中的变量一定是在某一个代码块内的。注意,这里的“某一个代码块内”并不包含它的任何子代码块,否则就变成了“多个代码块之间”。而可重名变量指的正是在多个代码块之间由相同的标识符代表的变量。
(2)变量重声明是对同一个变量的多次声明,这里的变量只有一个。而可重名变量中涉及的变量肯定是有多个的。
(3)不论对变量重声明多少次,其类型必须始终一致,具体遵从它第一次被声明时给定的类型。而可重名变量之间不存在类似的限制,它们的类型可以是任意的。
(4)如果可重名变量所在的代码块之间,存在直接或间接的嵌套关系,那么它们之间一定会存在“屏蔽”的现象。但是这种现象绝对不会在变量重声明的场景下出现。
既然可重名变量的类型可以是任意的,那么当它们之间存在“屏蔽”时你就更需要注意了。不同类型的值大都有着不同的特性和用法。当你在某一种类型的值上施加只有在其他类型值上才能做的操作时,Go 语言编译器一定会告诉你:“这不可以”。
看个例子,两个都叫做container的变量,分别位于main包代码块和main函数代码块。main包代码块中的变量是切片(slice)类型的,另一个是字典(map)类型的。在main函数的最后,我试图打印出container变量的值中索引为1的那个元素:
package main
import "fmt"
var container = []string{"zero", "one", "two"}
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
fmt.Printf("The element is %q.\n", container[1])
}
go run demo11.go
The element is "one".
如果修改下代码,把:
package main
import "fmt"
var container = []string{"zero", "one", "two"}
func main() {
container := map[int]string{0: "zero", 1: "1", 2: "two"} //这里 修改1:"one" 为 1:"1"
fmt.Printf("The element is %q.\n", container[1])
}
输出是1,说明代码执行使用的内层{ }代码块中的变量。
go run demo11.go
The element is "1".
答案是使用“类型断言”表达式。具体怎么写呢?
value, ok := interface{}(container).([]string)
赋值语句的赋值符号的右边,是一个类型断言表达式,它包括了用来把container变量的值转换为空接口值的interface{}(container)。以及一个用于判断前者的类型是否为切片类型 []string 的 .([]string)。
这个表达式的结果可以被赋给两个变量,在这里由value和ok代表。变量ok是布尔(bool)类型的,它将代表类型判断的结果,true或false。
如果是true,那么被判断的值将会被自动转换为[]string类型的值,并赋给变量value,否则value将被赋予nil(即“空”)。
顺便提一下,这里的ok也可以没有。也就是说,类型断言表达式的结果,可以只被赋给一个变量,在这里是value。但是这样的话,当判断为否时就会引发异常。
类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的值。这个值当下的类型必须是接口类型的,不过具体是哪个接口类型其实是无所谓的。所以,当这里的container变量类型不是任何的接口类型时,我们就需要先把它转成某个接口类型的值。
如果container是某个接口类型的,那么这个类型断言表达式就可以是container.([]string)。这样看是不是清晰一些了?
interface{}代表空接口,任何类型都是它的实现类型。我在下个模块,会再讲接口及其实现类型的问题。现在你只要知道,任何类型的值都可以很方便地被转换成空接口的值就行了。
你可能会对这里的{}产生疑惑,为什么在关键字interface的右边还要加上这个东西?
请记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。
比如你今后肯定会遇到的struct{},它就代表了不包含任何字段和方法的、空的结构体类型。而空接口interface{}则代表了不包含任何方法定义的、空的接口类型。当然了,对于一些集合类的数据类型来说,{}还可以用来表示其值不包含任何元素,比如空的切片值[]string{},以及空的字典值map[int]string{}。
最右边看。圆括号中[]string是一个类型字面量。所谓类型字面量,就是用来表示数据类型本身的若干个字符。
比如,string是表示字符串类型的字面量,uint8是表示 8 位无符号整数类型的字面量。
再复杂一些的就是我们刚才提到的[]string,用来表示元素类型为string的切片类型,以及map[int]string,用来表示键类型为int、值类型为string的字典类型。
首先,对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。比如,之所以uint8(255)可以把无类型的常量255转换为uint8类型的值,是因为255在 [0, 255] 的范围内。但需要特别注意的是,源整数类型的可表示范围较大,而目标类型的可表示范围较小的情况,比如把值的类型从int16转换为int8。请看下面这段代码:
var srcInt = int16(-255)
dstInt := int8(srcInt)
变量srcInt的值是int16类型的-255,而变量dstInt的值是由前者转换而来的,类型是int8。int16类型的可表示范围可比int8类型大了不少。
问题是,dstInt的值是多少?首先你要知道,整数在 Go 语言以及计算机中都是以补码的形式存储的。这主要是为了简化计算机对整数的运算过程。补码其实就是原码各位求反再加 1。比如,int16类型的值-255的补码是1111111100000001。如果我们把该值转换为int8类型的值,那么 Go 语言会把在较高位置(或者说最左边位置)上的 8 位二进制数直接截掉,从而得到00000001。又由于其最左边一位是0,表示它是个正整数,以及正整数的补码就等于其原码,所以dstInt的值就是1。
一定要记住,当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。
类似的快刀斩乱麻规则还有:当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉。
第二,虽然直接把一个整数值转换为一个string类型的值是可行的,但值得关注的是,被转换的整数值应该可以代表一个有效的 Unicode 代码点,否则转换的结果将会是"�"(仅由高亮的问号组成的字符串值)。
字符'�'的 Unicode 代码点是U+FFFD。它是 Unicode 标准中定义的 Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。我肯定不会去问“哪个整数值转换后会得到哪个字符串”,这太变态了!但是我会写下:
string(-1)
并询问会得到什么?这可是完全不同的问题啊。由于-1肯定无法代表一个有效的 Unicode 代码点,所以得到的总会是"�"。在实际工作中,我们在排查问题时可能会遇到�,你需要知道这可能是由于什么引起的。
第三个知识点是关于string类型与各种切片类型之间的互转的。
你先要理解的是,一个值在从string类型向[]byte类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节。除了与 ASCII 编码兼容的那部分字符集,以 UTF-8 编码的某个单一字节是无法代表一个字符的。
string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好
比如,UTF-8 编码的三个字节\xe4、\xbd和\xa0合在一起才能代表字符'你',而\xe5、\xa5和\xbd合在一起才能代表字符'好'。
其次,一个值在从string类型向[]rune类型转换时代表着字符串会被拆分成一个个 Unicode 字符。
string([]rune{'\u4F60', '\u597D'}) // 你好
当你真正理解了 Unicode 标准及其字符集和编码方案之后,上面这些内容就会显得很容易了。什么是 Unicode 标准?我会首先推荐你去它的http://www.unicode.org/ 官方网站一探究竟。
我们可以用关键字type声明自定义的各种类型。当然了,这些类型必须在 Go 语言基本类型和高级类型的范畴之内。在它们当中,有一种被叫做“别名类型”的类型。我们可以像下面这样声明它:
type MyString = string
这条声明语句表示,MyString是string类型的别名类型。顾名思义,别名类型与其源类型的区别恐怕只是在名称上,它们是完全相同的。源类型与别名类型是一对概念,是两个对立的称呼。别名类型主要是为了代码重构而存在的
Go 语言内建的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32的别名类型。
一定要注意,如果我这样声明:
type MyString2 string // 注意,这里没有等号。
MyString2和string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其他任何类型。这种方式也可以被叫做对类型的再定义。我们刚刚把string类型再定义成了另外一个类型MyString2。
对于这里的类型再定义来说,string可以被称为MyString2的潜在类型。潜在类型的含义是,某个类型在本质上是哪个类型。
潜在类型相同的不同类型的值之间是可以进行类型转换的。因此,MyString2类型的值与string类型的值可以使用类型转换表达式进行互转。
但对于集合类的类型[]MyString2与[]string来说这样做却是不合法的,因为[]MyString2与[]string的潜在类型不同,分别是[]MyString2和[]string。另外,即使两个不同类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值。
package main
import (
"fmt"
)
var container = []string{"zero", "one", "two"}
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
// 方式1。
_, ok1 := interface{}(container).([]string)
_, ok2 := interface{}(container).(map[int]string)
if !(ok1 || ok2) {
fmt.Printf("Error: unsupported container type: %T\n", container)
return
}
fmt.Printf("The element is %q. (container type: %T)\n",
container[1], container)
// 方式2。
elem, err := getElement(container)
if err != nil {
fmt.Printf("Error: %s\n", err)
return
}
fmt.Printf("The element is %q. (container type: %T)\n",
elem, container)
}
func getElement(containerI interface{}) (elem string, err error) {
switch t := containerI.(type) {
case []string:
elem = t[1]
case map[int]string:
elem = t[1] //这里如果改为t[0],输出是zero
default:
err = fmt.Errorf("unsupported container type: %T", containerI)
return
}
return
}
go run demo12.go
The element is "one". (container type: map[int]string)
The element is "one". (container type: map[int]string)
package main
import (
"fmt"
)
func main() {
// 重点1的示例。
var srcInt = int16(-255)
// 请注意,之所以要执行uint16(srcInt),是因为只有这样才能得到全二进制的表示。
// 例如,fmt.Printf("%b", srcInt)将打印出"-11111111",后者是负数符号再加上srcInt的绝对值的补码。
// 而fmt.Printf("%b", uint16(srcInt))才会打印出srcInt原值的补码"1111111100000001"。
fmt.Printf("The complement of srcInt: %b (%b)\n",
uint16(srcInt), srcInt)
dstInt := int8(srcInt)
fmt.Printf("The complement of dstInt: %b (%b)\n",
uint8(dstInt), dstInt)
fmt.Printf("The value of dstInt: %d\n", dstInt)
fmt.Println()
// 重点2的示例。
fmt.Printf("The Replacement Character: %s\n", string(-1))
fmt.Printf("The Unicode codepoint of Replacement Character: %U\n", '�')
fmt.Println()
// 重点3的示例。
srcStr := "你好"
fmt.Printf("The string: %q\n", srcStr)
fmt.Printf("The hex of %q: %x\n", srcStr, srcStr)
fmt.Printf("The byte slice of %q: % x\n", srcStr, []byte(srcStr))
fmt.Printf("The string: %q\n", string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}))
fmt.Printf("The rune slice of %q: %U\n", srcStr, []rune(srcStr))
fmt.Printf("The string: %q\n", string([]rune{'\u4F60', '\u597D'}))
}
go run demo13.go
The complement of srcInt: 1111111100000001 (-11111111)
The complement of dstInt: 1 (1)
The value of dstInt: 1
The Replacement Character: �
The Unicode codepoint of Replacement Character: U+FFFD
The string: "你好"
The hex of "你好": e4bda0e5a5bd
The byte slice of "你好": e4 bd a0 e5 a5 bd
The string: "你好"
The rune slice of "你好": [U+4F60 U+597D]
The string: "你好"
package main
import "fmt"
func main() {
// 示例1。
{
type MyString = string
str := "BCD"
myStr1 := MyString(str)
myStr2 := MyString("A" + str)
fmt.Printf("%T(%q) == %T(%q): %v\n",
str, str, myStr1, myStr1, str == myStr1)
fmt.Printf("%T(%q) > %T(%q): %v\n",
str, str, myStr2, myStr2, str > myStr2)
fmt.Printf("Type %T is the same as type %T.\n", myStr1, str)
strs := []string{"E", "F", "G"}
myStrs := []MyString(strs)
fmt.Printf("A value of type []MyString: %T(%q)\n",
myStrs, myStrs)
fmt.Printf("Type %T is the same as type %T.\n", myStrs, strs)
fmt.Println()
}
// 示例2。
{
type MyString string
str := "BCD"
myStr1 := MyString(str)
myStr2 := MyString("A" + str)
_ = myStr2
//fmt.Printf("%T(%q) == %T(%q): %v\n",
// str, str, myStr1, myStr1, str == myStr1) // 这里的判等不合法,会引发编译错误。
//fmt.Printf("%T(%q) > %T(%q): %v\n",
// str, str, myStr2, myStr2, str > myStr2) // 这里的比较不合法,会引发编译错误。
fmt.Printf("Type %T is different from type %T.\n", myStr1, str)
strs := []string{"E", "F", "G"}
var myStrs []MyString
//myStrs := []MyString(strs) // 这里的类型转换不合法,会引发编译错误。
//fmt.Printf("A value of type []MyString: %T(%q)\n",
// myStrs, myStrs)
fmt.Printf("Type %T is different from type %T.\n", myStrs, strs)
fmt.Println()
}
// 示例3。
{
type MyString1 = string
type MyString2 string
str := "BCD"
myStr1 := MyString1(str)
myStr2 := MyString2(str)
myStr1 = MyString1(myStr2)
myStr2 = MyString2(myStr1)
myStr1 = str
//myStr2 = str // 这里的赋值不合法,会引发编译错误。
//myStr1 = myStr2 // 这里的赋值不合法,会引发编译错误。
//myStr2 = myStr1 // 这里的赋值不合法,会引发编译错误。
}
}
go run demo14.go
string("BCD") == string("BCD"): true
string("BCD") > string("ABCD"): true
Type string is the same as type string.
A value of type []MyString: []string(["E" "F" "G"])
Type []string is the same as type []string.
Type main.MyString is different from type string.
Type []main.MyString is different from type []string.
本文名称:学习笔记-go程序实体
网页URL:https://www.cdcxhl.com/article14/jhijde.html
成都网站建设公司_创新互联,为您提供ChatGPT、网站内链、微信公众号、Google、用户体验、云服务器
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联