-
Go 语言数组Go 语言提供了数组类型的数据结构。数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。相对于去声明 number0, number1, ..., number99 的变量,使用数组形式 numbers[0], numbers[1] ..., numbers[99] 更加方便且易于扩展。数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。声明数组Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:var variable_name [SIZE] variable_type以上为一维数组的定义方式。例如以下定义了数组 balance 长度为 10 类型为 float32:var balance [10] float32初始化数组以下演示了数组初始化:var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}我们也可以通过字面量在声明数组的同时快速初始化数组:balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}或balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}如果设置了数组的长度,我们还可以通过指定下标来初始化元素:// 将索引为 1 和 3 的元素初始化balance := [5]float32{1:2.0,3:7.0}初始化数组中 {} 中的元素个数不能大于 [] 中的数字。如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小: balance[4] = 50.0以上实例读取了第五个元素。数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。访问数组元素数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。例如:var salary float32 = balance[9]以上实例读取了数组 balance 第 10 个元素的值。以下演示了数组完整操作(声明、赋值、访问)的实例:实例 1package mainimport "fmt"func main() { var n [10]int /* n 是一个长度为 10 的数组 */ var i,j int /* 为数组 n 初始化元素 */ for i = 0; i < 10; i++ { n[i] = i + 100 /* 设置元素为 i + 100 */ } /* 输出每个数组元素的值 */ for j = 0; j < 10; j++ { fmt.Printf("Element[%d] = %d\n", j, n[j] ) }}以上实例执行结果如下:Element[0] = 100Element[1] = 101Element[2] = 102Element[3] = 103Element[4] = 104Element[5] = 105Element[6] = 106Element[7] = 107Element[8] = 108Element[9] = 109实例 2package mainimport "fmt"func main() { var i,j,k int // 声明数组的同时快速初始化数组 balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} /* 输出数组元素 */ ... for i = 0; i < 5; i++ { fmt.Printf("balance[%d] = %f\n", i, balance[i] ) } balance2 := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} /* 输出每个数组元素的值 */ for j = 0; j < 5; j++ { fmt.Printf("balance2[%d] = %f\n", j, balance2[j] ) } // 将索引为 1 和 3 的元素初始化 balance3 := [5]float32{1:2.0,3:7.0} for k = 0; k < 5; k++ { fmt.Printf("balance3[%d] = %f\n", k, balance3[k] ) }}以上实例执行结果如下:balance[0] = 1000.000000balance[1] = 2.000000balance[2] = 3.400000balance[3] = 7.000000balance[4] = 50.000000balance2[0] = 1000.000000balance2[1] = 2.000000balance2[2] = 3.400000balance2[3] = 7.000000balance2[4] = 50.000000balance3[0] = 0.000000balance3[1] = 2.000000balance3[2] = 0.000000balance3[3] = 7.000000balance3[4] = 0.000000
-
Go 语言变量作用域作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。Go 语言中变量可以在三个地方声明:函数内定义的变量称为局部变量函数外定义的变量称为全局变量函数定义中的变量称为形式参数接下来让我们具体了解局部变量、全局变量和形式参数。局部变量在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。以下实例中 main() 函数使用了局部变量 a, b, c:实例package mainimport "fmt"func main() { /* 声明局部变量 */ var a, b, c int /* 初始化参数 */ a = 10 b = 20 c = a + b fmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c)}以上实例执行输出结果为:结果: a = 10, b = 20 and c = 30全局变量在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。全局变量可以在任何函数中使用,以下实例演示了如何使用全局变量:实例package mainimport "fmt"/* 声明全局变量 */var g intfunc main() { /* 声明局部变量 */ var a, b int /* 初始化参数 */ a = 10 b = 20 g = a + b fmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)}以上实例执行输出结果为:结果: a = 10, b = 20 and g = 30Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。实例如下:实例package mainimport "fmt"/* 声明全局变量 */var g int = 20func main() { /* 声明局部变量 */ var g int = 10 fmt.Printf ("结果: g = %d\n", g)}以上实例执行输出结果为:结果: g = 10形式参数形式参数会作为函数的局部变量来使用。实例如下:实例package mainimport "fmt"/* 声明全局变量 */var a int = 20;func main() { /* main 函数中声明局部变量 */ var a int = 10 var b int = 20 var c int = 0 fmt.Printf("main()函数中 a = %d\n", a); c = sum( a, b); fmt.Printf("main()函数中 c = %d\n", c);}/* 函数定义-两数相加 */func sum(a, b int) int { fmt.Printf("sum() 函数中 a = %d\n", a); fmt.Printf("sum() 函数中 b = %d\n", b); return a + b;}以上实例执行输出结果为:main()函数中 a = 10sum() 函数中 a = 10sum() 函数中 b = 20main()函数中 c = 30
-
Go 语言函数函数是基本的代码块,用于执行一个任务。Go 语言最少有个 main() 函数。你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。函数声明告诉了编译器函数的名称,返回类型,和参数。Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。函数定义Go 语言函数定义格式如下:func function_name( [parameter list] ) [return_types] { 函数体}函数定义解析:func:函数由 func 开始声明function_name:函数名称,参数列表和返回值类型构成了函数签名。parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。函数体:函数定义的代码集合。实例以下实例为 max() 函数的代码,该函数传入两个整型参数 num1 和 num2,并返回这两个参数的最大值:实例/* 函数返回两个数的最大值 */func max(num1, num2 int) int { /* 声明局部变量 */ var result int if (num1 > num2) { result = num1 } else { result = num2 } return result }函数调用当创建函数时,你定义了函数需要做什么,通过调用该函数来执行指定任务。调用函数,向函数传递参数,并返回值,例如:实例package mainimport "fmt"func main() { /* 定义局部变量 */ var a int = 100 var b int = 200 var ret int /* 调用函数并返回最大值 */ ret = max(a, b) fmt.Printf( "最大值是 : %d\n", ret )}/* 函数返回两个数的最大值 */func max(num1, num2 int) int { /* 定义局部变量 */ var result int if (num1 > num2) { result = num1 } else { result = num2 } return result }以上实例在 main() 函数中调用 max()函数,执行结果为:最大值是 : 200函数返回多个值Go 函数可以返回多个值,例如:实例package mainimport "fmt"func swap(x, y string) (string, string) { return y, x}func main() { a, b := swap("Google", "Runoob") fmt.Println(a, b)}以上实例执行结果为:Runoob Google
-
Go 语言条件语句条件语句需要开发者通过指定一个或多个条件,并通过测试条件是否为 true 来决定是否执行指定语句,并在条件为 false 的情况在执行另外的语句。下图展示了程序语言中条件语句的结构:Go 语言提供了以下几种条件判断语句:语句 描述if 语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。if...else 语句 if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。if 嵌套语句 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。switch 语句 switch 语句用于基于不同条件执行不同动作。select 语句 select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。注意:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。Go 语言循环语句在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。Go 语言提供了以下几种类型循环处理语句:循环类型 描述for 循环 重复执行语句块循环嵌套 在 for 循环中嵌套一个或多个 for 循环循环控制语句循环控制语句可以控制循环体内语句的执行过程。GO 语言支持以下几种循环控制语句:控制语句 描述break 语句 经常用于中断当前 for 循环或跳出 switch 语句continue 语句 跳过当前循环的剩余语句,然后继续进行下一轮循环。goto 语句 将控制转移到被标记的语句。无限循环如果循环中条件语句永远不为 false 则会进行无限循环,我们可以通过 for 循环语句中只设置一个条件表达式来执行无限循环:实例package mainimport "fmt"func main() { for true { fmt.Printf("这是无限循环。\n"); }}
-
Go 语言常量常量是一个简单值的标识符,在程序运行时,不会被修改的量。常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。常量的定义格式:const identifier [type] = value你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。显式类型定义: const b string = "abc"隐式类型定义: const b = "abc"多个相同类型的声明可以简写为:const c_name1, c_name2 = value1, value2以下实例演示了常量的应用:实例package mainimport "fmt"func main() { const LENGTH int = 10 const WIDTH int = 5 var area int const a, b, c = 1, false, "str" //多重赋值 area = LENGTH * WIDTH fmt.Printf("面积为 : %d", area) println() println(a, b, c) }以上实例运行结果为:面积为 : 501 false str常量还可以用作枚举:const ( Unknown = 0 Female = 1 Male = 2)数字 0、1 和 2 分别代表未知性别、女性和男性。常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:实例package mainimport "unsafe"const ( a = "abc" b = len(a) c = unsafe.Sizeof(a))func main(){ println(a, b, c)}以上实例运行结果为:abc 3 16iotaiota,特殊常量,可以认为是一个可以被编译器修改的常量。iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。iota 可以被用作枚举值:const ( a = iota b = iota c = iota)第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:const ( a = iota b c)iota 用法实例package mainimport "fmt"func main() { const ( a = iota //0 b //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i //8 ) fmt.Println(a,b,c,d,e,f,g,h,i)}以上实例运行结果为:0 1 2 ha ha 100 100 7 8再看个有趣的的 iota 实例:实例package mainimport "fmt"const ( i=1<<iota j=3<<iota k l)func main() { fmt.Println("i=",i) fmt.Println("j=",j) fmt.Println("k=",k) fmt.Println("l=",l)}以上实例运行结果为:i= 1j= 6k= 12l= 24iota 表示从 0 开始自动加 1,所以 i=1<<0, j=3<<1(<< 表示左移的意思),即:i=1, j=6,这没问题,关键在 k 和 l,从输出结果看 k=3<<2,l=3<<3。
-
Go 语言变量变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。变量可以通过变量名访问。Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。声明变量的一般形式是使用 var 关键字:var identifier type可以一次声明多个变量:var identifier1, identifier2 type实例package mainimport "fmt"func main() { var a string = "Runoob" fmt.Println(a) var b, c int = 1, 2 fmt.Println(b, c)}以上实例输出结果为:Runoob1 2变量声明第一种,指定变量类型,如果没有初始化,则变量默认为零值。var v_name v_typev_name = value零值就是变量没有做初始化时系统默认设置的值。实例package mainimport "fmt"func main() { // 声明一个变量并初始化 var a = "RUNOOB" fmt.Println(a) // 没有初始化就为零值 var b int fmt.Println(b) // bool 零值为 false var c bool fmt.Println(c)}以上实例执行结果为:RUNOOB0false数值类型(包括complex64/128)为 0布尔类型为 false字符串为 ""(空字符串)以下几种类型为 nil:var a *intvar a []intvar a map[string] intvar a chan intvar a func(string) intvar a error // error 是接口实例package mainimport "fmt"func main() { var i int var f float64 var b bool var s string fmt.Printf("%v %v %v %q\n", i, f, b, s)}输出结果是:0 0 false ""第二种,根据值自行判定变量类型。var v_name = value实例package mainimport "fmt"func main() { var d = true fmt.Println(d)}输出结果是:true
-
Go 标记Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:fmt.Println("Hello, World!")6 个标记是(每行一个):1. fmt2. .3. Println4. (5. "Hello, World!"6. )行分隔符在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。以下为两个语句:fmt.Println("Hello, World!")fmt.Println(":runoob.com")注释注释不会被编译,每一个包应该有相关注释。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾。如:// 单行注释/* Author by 菜鸟教程 我是多行注释 */标识符标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(A~Z和a~z)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。以下是有效的标识符:mahesh kumar abc move_name a_123myname50 _temp j a23b9 retVal以下是无效的标识符:1ab(以数字开头)case(Go 语言的关键字)a+b(运算符是不允许的)字符串连接Go 语言的字符串可以通过 + 实现:实例package mainimport "fmt"func main() { fmt.Println("Google" + "Runoob")}
-
Go Hello World 实例Go 语言的基础组成有以下几个部分:包声明引入包函数变量语句 & 表达式注释接下来让我们来看下简单的代码,该代码输出了"Hello World!":实例package mainimport "fmt"func main() { /* 这是我的第一个简单的程序 */ fmt.Println("Hello, World!")}让我们来看下以上程序的各个部分:第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。下一行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。下一行 /*...*/ 是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。下一行 fmt.Println(...) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。 使用 fmt.Print("hello, world\n") 可以得到相同的结果。 Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。执行 Go 程序让我们来看下如何编写 Go 代码并执行它。步骤如下:打开编辑器如Sublime2,将以上代码添加到编辑器中。将以上代码保存为 hello.go打开命令行,并进入程序文件保存的目录中。输入命令 go run hello.go 并按回车执行代码。如果操作正确你将在屏幕上看到 "Hello World!" 字样的输出。$ go run hello.goHello, World!我们还可以使用 go build 命令来生成二进制文件:$ go build hello.go $ lshello hello.go$ ./hello Hello, World!注意需要注意的是 { 不能单独放在一行,所以以下代码在运行时会产生错误:实例package mainimport "fmt"func main() { // 错误,{ 不能在单独的行上 fmt.Println("Hello, World!")}
-
回复本贴查看下载完整版PPT点击 →《华为云DevCloud大咖分享汇总(附PPT下载)》[hide]https://devcloud.cn-north-4.huaweicloud.com/codehub/project/8e8846873de740e396de90d8182b63fd/codehub/1284981/home/commit/4c77cfc5d0ea1b56a91e5df12605bb5f508a503d?ref=master&filePath=Gopher%20China2020%20%E5%8D%8E%E4%B8%BA%E4%BA%91%E7%9A%84go%E8%AF%AD%E8%A8%80%E4%BA%91%E5%8E%9F%E7%94%9F%E5%AE%9E%E6%88%98%E7%BB%8F%E9%AA%8C.pdf&isFile=true[/hide]
-
【摘要】 设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。前言从1995年GoF提出23种设计模式到现在,25年过去了,设计模式依旧是软件领域的热门话题。在当下,如果你不会一点设计模式,都不好意思说自己是一个合格的程序员。设计模式通常被定义为:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。从定义上看,设计模式其实是一种经验的总结,是针对特定问题的简洁而优雅的解决方案。既然是经验总结,那么学习设计模式最直接的好处就在于可以站在巨人的肩膀上解决软件开发过程中的一些特定问题。然而,学习设计模式的最高境界是习得其中解决问题所用到的思想,当你把它们的本质思想吃透了,也就能做到即使已经忘掉某个设计模式的名称和结构,也能在解决特定问题时信手拈来。好的东西有人吹捧,当然也会招黑。设计模式被抨击主要因为以下两点:1、设计模式会增加代码量,把程序逻辑变得复杂。这一点是不可避免的,但是我们并不能仅仅只考虑开发阶段的成本。最简单的程序当然是一个函数从头写到尾,但是这样后期的维护成本会变得非常大;而设计模式虽然增加了一点开发成本,但是能让人们写出可复用、可维护性高的程序。引用《软件设计的哲学》里的概念,前者就是战术编程,后者就是战略编程,我们应该对战术编程Say No!(请移步《专家教你系列<三>:写出的代码复杂度太高?看下专家怎么说》)2、滥用设计模式。这是初学者最容易犯的错误,当学到一个模式时,恨不得在所有的代码都用上,从而在不该使用模式的地方刻意地使用了模式,导致了程序变得异常复杂。其实每个设计模式都有几个关键要素:适用场景、解决方法、优缺点。模式并不是万能药,它只有在特定的问题上才能显现出效果。所以,在使用一个模式前,先问问自己,当前的这个场景适用这个模式吗?《设计模式》一书的副标题是“可复用面向对象软件的基础”,但并不意味着只有面向对象语言才能使用设计模式。模式只是一种解决特定问题的思想,跟语言无关。就像Go语言一样,它并非是像C++和Java一样的面向对象语言,但是设计模式同样适用。本系列文章将使用Go语言来实现GoF提出的23种设计模式,按照创建型模式(Creational Pattern)、结构型模式(Structural Pattern)和行为型模式(Behavioral Pattern)三种类别进行组织,文本主要介绍其中的创建型模式。单例模式(Singleton Pattern)简述单例模式算是23中设计模式里最简单的一个了,它主要用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。在程序设计中,有一些对象通常我们只需要一个共享的实例,比如线程池、全局缓存、对象池等,这种场景下就适合使用单例模式。但是,并非所有全局唯一的场景都适合使用单例模式。比如,考虑需要统计一个API调用的情况,有两个指标,成功调用次数和失败调用次数。这两个指标都是全局唯一的,所以有人可能会将其建模成两个单例SuccessApiMetric和FailApiMetric。按照这个思路,随着指标数量的增多,你会发现代码里类的定义会越来越多,也越来越臃肿。这也是单例模式最常见的误用场景,更好的方法是将两个指标设计成一个对象ApiMetric下的两个实例ApiMetic success和ApiMetic fail。如何判断一个对象是否应该被建模成单例?通常,被建模成单例的对象都有“中心点”的含义,比如线程池就是管理所有线程的中心。所以,在判断一个对象是否适合单例模式时,先思考下,这个对象是一个中心点吗?Go实现在对某个对象实现单例模式时,有两个点必须要注意:(1)限制调用者直接实例化该对象;(2)为该对象的单例提供一个全局唯一的访问方法。对于C++/Java而言,只需把类的构造函数设计成私有的,并提供一个static方法去访问该类点唯一实例即可。但对于Go语言来说,即没有构造函数的概念,也没有static方法,所以需要另寻出路。我们可以利用Go语言package的访问规则来实现,将单例结构体设计成首字母小写,就能限定其访问范围只在当前package下,模拟了C++/Java中的私有构造函数;再在当前package下实现一个首字母大写的访问函数,就相当于static方法的作用了。在实际开发中,我们经常会遇到需要频繁创建和销毁的对象。频繁的创建和销毁一则消耗CPU,二则内存的利用率也不高,通常我们都会使用对象池技术来进行优化。考虑我们需要实现一个消息对象池,因为是全局的中心点,管理所有的Message实例,所以将其实现成单例,实现代码如下: package msgpool ... // 消息池 type messagePool struct { pool *sync.Pool } // 消息池单例 var msgPool = &messagePool{ // 如果消息池里没有消息,则新建一个Count值为0的Message实例 pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }}, } // 访问消息池单例的唯一方法 func Instance() *messagePool { return msgPool } // 往消息池里添加消息 func (m *messagePool) AddMsg(msg *Message) { m.pool.Put(msg) } // 从消息池里获取消息 func (m *messagePool) GetMsg() *Message { return m.pool.Get().(*Message) } ...测试代码如下: package test ... func TestMessagePool(t *testing.T) { msg0 := msgpool.Instance().GetMsg() if msg0.Count != 0 { t.Errorf("expect msg count %d, but actual %d.", 0, msg0.Count) } msg0.Count = 1 msgpool.Instance().AddMsg(msg0) msg1 := msgpool.Instance().GetMsg() if msg1.Count != 1 { t.Errorf("expect msg count %d, but actual %d.", 1, msg1.Count) } } // 运行结果 === RUN TestMessagePool --- PASS: TestMessagePool (0.00s) PASS以上的单例模式就是典型的“饿汉模式”,实例在系统加载的时候就已经完成了初始化。对应地,还有一种“懒汉模式”,只有等到对象被使用的时候,才会去初始化它,从而一定程度上节省了内存。众所周知,“懒汉模式”会带来线程安全问题,可以通过普通加锁,或者更高效的双重检验锁来优化。对于“懒汉模式”,Go语言有一个更优雅的实现方式,那就是利用sync.Once,它有一个Do方法,其入参是一个方法,Go语言会保证仅仅只调用一次该方法。 // 单例模式的“懒汉模式”实现 package msgpool ... var once = &sync.Once{} // 消息池单例,在首次调用时初始化 var msgPool *messagePool // 全局唯一获取消息池pool到方法 func Instance() *messagePool { // 在匿名函数中实现初始化逻辑,Go语言保证只会调用一次 once.Do(func() { msgPool = &messagePool{ // 如果消息池里没有消息,则新建一个Count值为0的Message实例 pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }}, } }) return msgPool } ...建造者模式(Builder Pattern)简述在程序设计中,我们会经常遇到一些复杂的对象,其中有很多成员属性,甚至嵌套着多个复杂的对象。这种情况下,创建这个复杂对象就会变得很繁琐。对于C++/Java而言,最常见的表现就是构造函数有着长长的参数列表: MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)而对于Go语言来说,最常见的表现就是多层的嵌套实例化: obj := &MyObject{ Field1: &Field1 { Param1: &Param1 { Val: 0, }, Param2: &Param2 { Val: 1, }, ... }, Field2: &Field2 { Param3: &Param3 { Val: 2, }, ... }, ... }上述的对象创建方法有两个明显的缺点:(1)对对象使用者不友好,使用者在创建对象时需要知道的细节太多;(2)代码可读性很差。针对这种对象成员较多,创建对象逻辑较为繁琐的场景,就适合使用建造者模式来进行优化。建造者模式的作用有如下几个:1、封装复杂对象的创建过程,使对象使用者不感知复杂的创建逻辑。2、可以一步步按照顺序对成员进行赋值,或者创建嵌套对象,并最终完成目标对象的创建。3、对多个对象复用同样的对象创建逻辑。其中,第1和第2点比较常用,下面对建造者模式的实现也主要是针对这两点进行示例。Go实现考虑如下的一个Message结构体,其主要有Header和Body组成: package msg ... type Message struct { Header *Header Body *Body } type Header struct { SrcAddr string SrcPort uint64 DestAddr string DestPort uint64 Items map[string]string } type Body struct { Items []string } ...如果按照直接的对象创建方式,创建逻辑应该是这样的: // 多层的嵌套实例化 message := msg.Message{ Header: &msg.Header{ SrcAddr: "192.168.0.1", SrcPort: 1234, DestAddr: "192.168.0.2", DestPort: 8080, Items: make(map[string]string), }, Body: &msg.Body{ Items: make([]string, 0), }, } // 需要知道对象的实现细节 message.Header.Items["contents"] = "application/json" message.Body.Items = append(message.Body.Items, "record1") message.Body.Items = append(message.Body.Items, "record2")虽然Message结构体嵌套的层次不多,但是从其创建的代码来看,确实存在对对象使用者不友好和代码可读性差的缺点。下面我们引入建造者模式对代码进行重构: package msg ... // Message对象的Builder对象 type builder struct { once *sync.Once msg *Message } // 返回Builder对象 func Builder() *builder { return &builder{ once: &sync.Once{}, msg: &Message{Header: &Header{}, Body: &Body{}}, } } // 以下是对Message成员对构建方法 func (b *builder) WithSrcAddr(srcAddr string) *builder { b.msg.Header.SrcAddr = srcAddr return b } func (b *builder) WithSrcPort(srcPort uint64) *builder { b.msg.Header.SrcPort = srcPort return b } func (b *builder) WithDestAddr(destAddr string) *builder { b.msg.Header.DestAddr = destAddr return b } func (b *builder) WithDestPort(destPort uint64) *builder { b.msg.Header.DestPort = destPort return b } func (b *builder) WithHeaderItem(key, value string) *builder { // 保证map只初始化一次 b.once.Do(func() { b.msg.Header.Items = make(map[string]string) }) b.msg.Header.Items[key] = value return b } func (b *builder) WithBodyItem(record string) *builder { b.msg.Body.Items = append(b.msg.Body.Items, record) return b } // 创建Message对象,在最后一步调用 func (b *builder) Build() *Message { return b.msg }测试代码如下: package test ... func TestMessageBuilder(t *testing.T) { // 使用消息建造者进行对象创建 message := msg.Builder(). WithSrcAddr("192.168.0.1"). WithSrcPort(1234). WithDestAddr("192.168.0.2"). WithDestPort(8080). WithHeaderItem("contents", "application/json"). WithBodyItem("record1"). WithBodyItem("record2"). Build() if message.Header.SrcAddr != "192.168.0.1" { t.Errorf("expect src address 192.168.0.1, but actual %s.", message.Header.SrcAddr) } if message.Body.Items[0] != "record1" { t.Errorf("expect body item0 record1, but actual %s.", message.Body.Items[0]) } } // 运行结果 === RUN TestMessageBuilder --- PASS: TestMessageBuilder (0.00s) PASS从测试代码可知,使用建造者模式来进行对象创建,使用者不再需要知道对象具体的实现细节,代码可读性也更好。工厂方法模式(Factory Method Pattern)简述工厂方法模式跟上一节讨论的建造者模式类似,都是将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口。两者在应用场景上稍有区别,建造者模式更常用于需要传递多个参数来进行实例化的场景。使用工厂方法来创建对象主要有两个好处:1、代码可读性更好。相比于使用C++/Java中的构造函数,或者Go中的{}来创建对象,工厂方法因为可以通过函数名来表达代码含义,从而具备更好的可读性。比如,使用工厂方法productA := CreateProductA()创建一个ProductA对象,比直接使用productA := ProductA{}的可读性要好。2、与使用者代码解耦。很多情况下,对象的创建往往是一个容易变化的点,通过工厂方法来封装对象的创建过程,可以在创建逻辑变更时,避免霰弹式修改。工厂方法模式也有两种实现方式:(1)提供一个工厂对象,通过调用工厂对象的工厂方法来创建产品对象;(2)将工厂方法集成到产品对象中(C++/Java中对象的static方法,Go中同一package下的函数)Go实现考虑有一个事件对象Event,分别有两种有效的时间类型Start和End: package event ... type Type uint8 // 事件类型定义 const ( Start Type = iota End ) // 事件抽象接口 type Event interface { EventType() Type Content() string } // 开始事件,实现了Event接口 type StartEvent struct{ content string } ... // 结束事件,实现了Event接口 type EndEvent struct{ content string } ...1、按照第一种实现方式,为Event提供一个工厂对象,具体代码如下: package event ... // 事件工厂对象 type Factory struct{} // 更具事件类型创建具体事件 func (e *Factory) Create(etype Type) Event { switch etype { case Start: return &StartEvent{ content: "this is start event", } case End: return &EndEvent{ content: "this is end event", } default: return nil } }测试代码如下: package test ... func TestEventFactory(t *testing.T) { factory := event.Factory{} e := factory.Create(event.Start) if e.EventType() != event.Start { t.Errorf("expect event.Start, but actual %v.", e.EventType()) } e = factory.Create(event.End) if e.EventType() != event.End { t.Errorf("expect event.End, but actual %v.", e.EventType()) } } // 运行结果 === RUN TestEventFactory --- PASS: TestEventFactory (0.00s) PASS2、按照第二种实现方式,分别给Start和End类型的Event单独提供一个工厂方法,代码如下: package event ... // Start类型Event的工厂方法 func OfStart() Event { return &StartEvent{ content: "this is start event", } } // End类型Event的工厂方法 func OfEnd() Event { return &EndEvent{ content: "this is end event", } }测试代码如下: package event ... func TestEvent(t *testing.T) { e := event.OfStart() if e.EventType() != event.Start { t.Errorf("expect event.Start, but actual %v.", e.EventType()) } e = event.OfEnd() if e.EventType() != event.End { t.Errorf("expect event.End, but actual %v.", e.EventType()) } } // 运行结果 === RUN TestEvent --- PASS: TestEvent (0.00s) PASS抽象工厂模式(Abstract Factory Pattern)简述在工厂方法模式中,我们通过一个工厂对象来创建一个产品族,具体创建哪个产品,则通过swtich-case的方式去判断。这也意味着该产品组上,每新增一类产品对象,都必须修改原来工厂对象的代码;而且随着产品的不断增多,工厂对象的职责也越来越重,违反了单一职责原则。抽象工厂模式通过给工厂类新增一个抽象层解决了该问题,如上图所示,FactoryA和FactoryB都实现·抽象工厂接口,分别用于创建ProductA和ProductB。如果后续新增了ProductC,只需新增一个FactoryC即可,无需修改原有的代码;因为每个工厂只负责创建一个产品,因此也遵循了单一职责原则。Go实现考虑需要如下一个插件架构风格的消息处理系统,pipeline是消息处理的管道,其中包含了input、filter和output三个插件。我们需要实现根据配置来创建pipeline ,加载插件过程的实现非常适合使用工厂模式,其中input、filter和output三类插件的创建使用抽象工厂模式,而pipeline的创建则使用工厂方法模式。各类插件和pipeline的接口定义如下: package plugin ... // 插件抽象接口定义 type Plugin interface {} // 输入插件,用于接收消息 type Input interface { Plugin Receive() string } // 过滤插件,用于处理消息 type Filter interface { Plugin Process(msg string) string } // 输出插件,用于发送消息 type Output interface { Plugin Send(msg string) } package pipeline ... // 消息管道的定义 type Pipeline struct { input plugin.Input filter plugin.Filter output plugin.Output } // 一个消息的处理流程为 input -> filter -> output func (p *Pipeline) Exec() { msg := p.input.Receive() msg = p.filter.Process(msg) p.output.Send(msg) }接着,我们定义input、filter、output三类插件接口的具体实现: package plugin ... // input插件名称与类型的映射关系,主要用于通过反射创建input对象 var inputNames = make(map[string]reflect.Type) // Hello input插件,接收“Hello World”消息 type HelloInput struct {} func (h *HelloInput) Receive() string { return "Hello World" } // 初始化input插件映射关系表 func init() { inputNames["hello"] = reflect.TypeOf(HelloInput{}) } package plugin ... // filter插件名称与类型的映射关系,主要用于通过反射创建filter对象 var filterNames = make(map[string]reflect.Type) // Upper filter插件,将消息全部字母转成大写 type UpperFilter struct {} func (u *UpperFilter) Process(msg string) string { return strings.ToUpper(msg) } // 初始化filter插件映射关系表 func init() { filterNames["upper"] = reflect.TypeOf(UpperFilter{}) } package plugin ... // output插件名称与类型的映射关系,主要用于通过反射创建output对象 var outputNames = make(map[string]reflect.Type) // Console output插件,将消息输出到控制台上 type ConsoleOutput struct {} func (c *ConsoleOutput) Send(msg string) { fmt.Println(msg) } // 初始化output插件映射关系表 func init() { outputNames["console"] = reflect.TypeOf(ConsoleOutput{}) }然后,我们定义插件抽象工厂接口,以及对应插件的工厂实现: package plugin ... // 插件抽象工厂接口 type Factory interface { Create(conf Config) Plugin } // input插件工厂对象,实现Factory接口 type InputFactory struct{} // 读取配置,通过反射机制进行对象实例化 func (i *InputFactory) Create(conf Config) Plugin { t, _ := inputNames[conf.Name] return reflect.New(t).Interface().(Plugin) } // filter和output插件工厂实现类似 type FilterFactory struct{} func (f *FilterFactory) Create(conf Config) Plugin { t, _ := filterNames[conf.Name] return reflect.New(t).Interface().(Plugin) } type OutputFactory struct{} func (o *OutputFactory) Create(conf Config) Plugin { t, _ := outputNames[conf.Name] return reflect.New(t).Interface().(Plugin) }最后定义pipeline的工厂方法,调用plugin.Factory抽象工厂完成pipelien对象的实例化: package pipeline ... // 保存用于创建Plugin的工厂实例,其中map的key为插件类型,value为抽象工厂接口 var pluginFactories = make(map[plugin.Type]plugin.Factory) // 根据plugin.Type返回对应Plugin类型的工厂实例 func factoryOf(t plugin.Type) plugin.Factory { factory, _ := pluginFactories[t] return factory } // pipeline工厂方法,根据配置创建一个Pipeline实例 func Of(conf Config) *Pipeline { p := &Pipeline{} p.input = factoryOf(plugin.InputType).Create(conf.Input).(plugin.Input) p.filter = factoryOf(plugin.FilterType).Create(conf.Filter).(plugin.Filter) p.output = factoryOf(plugin.OutputType).Create(conf.Output).(plugin.Output) return p } // 初始化插件工厂对象 func init() { pluginFactories[plugin.InputType] = &plugin.InputFactory{} pluginFactories[plugin.FilterType] = &plugin.FilterFactory{} pluginFactories[plugin.OutputType] = &plugin.OutputFactory{} }测试代码如下: package test ... func TestPipeline(t *testing.T) { // 其中pipeline.DefaultConfig()的配置内容见【抽象工厂模式示例图】 // 消息处理流程为 HelloInput -> UpperFilter -> ConsoleOutput p := pipeline.Of(pipeline.DefaultConfig()) p.Exec() } // 运行结果 === RUN TestPipeline HELLO WORLD --- PASS: TestPipeline (0.00s) PASS原型模式(Prototype Pattern)简述原型模式主要解决对象复制的问题,它的核心就是clone()方法,返回Prototype对象的复制品。在程序设计过程中,往往会遇到有一些场景需要大量相同的对象,如果不使用原型模式,那么我们可能会这样进行对象的创建:新创建一个相同对象的实例,然后遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。这种方法的缺点很明显,那就是使用者必须知道对象的实现细节,导致代码之间的耦合。另外,对象很有可能存在除了对象本身以外不可见的变量,这种情况下该方法就行不通了。对于这种情况,更好的方法就是使用原型模式,将复制逻辑委托给对象本身,这样,上述两个问题也都迎刃而解了。Go实现还是以建造者模式一节中的Message作为例子,现在设计一个Prototype抽象接口: package prototype ... // 原型复制抽象接口 type Prototype interface { clone() Prototype } type Message struct { Header *Header Body *Body } func (m *Message) clone() Prototype { msg := *m return &msg }测试代码如下: package test ... func TestPrototype(t *testing.T) { message := msg.Builder(). WithSrcAddr("192.168.0.1"). WithSrcPort(1234). WithDestAddr("192.168.0.2"). WithDestPort(8080). WithHeaderItem("contents", "application/json"). WithBodyItem("record1"). WithBodyItem("record2"). Build() // 复制一份消息 newMessage := message.Clone().(*msg.Message) if newMessage.Header.SrcAddr != message.Header.SrcAddr { t.Errorf("Clone Message failed.") } if newMessage.Body.Items[0] != message.Body.Items[0] { t.Errorf("Clone Message failed.") } } // 运行结果 === RUN TestPrototype --- PASS: TestPrototype (0.00s) PASS总结本文主要介绍了GoF的23种设计模式中的5种创建型模式,创建型模式的目的都是提供一个简单的接口,让对象的创建过程与使用者解耦。其中,单例模式主要用于保证一个类仅有一个实例,并提供一个访问它的全局访问点;建造者模式主要解决需要创建对象时需要传入多个参数,或者对初始化顺序有要求的场景;工厂方法模式通过提供一个工厂对象或者工厂方法,为使用者隐藏了对象创建的细节;抽象工厂模式是对工厂方法模式的优化,通过为工厂对象新增一个抽象层,让工厂对象遵循单一职责原则,也避免了霰弹式修改;原型模式则让对象复制更加简单。下一篇文章,将介绍23种设计模式中的7种结构型模式(Structural Pattern),及其Go语言的实现。
-
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。首先将四个数组分割为两两数组,前两个数组值相加,后两个数组相加,入股前两个数组相加和与后两个数组相加和正好为相反数,四个元素之和为0.首先将两数组的元素进行遍历相加,相加之和为map的索引。所指向的元素,就是出现的次数。12345678func foursumcount(A []int, B []int, C []int, D []int) int{ des :=map[int]int{} for _,v:=range A{ for _,w:=range B{ des[v+w]++ } }}再次遍历另两个数组,将两个数组的元素进行相加,取和的相反数,通过使用相反数在map中查找,如果没出现,所指向的数是0,如果出现过这个数的相反数,则所指向的数大于一。123456789func foursumcount(A []int, B []int, C []int, D []int) int{ des :=map[int]int{} ans:=0 for _,v:=range C{ for _,w:=range D{ ans +=des[-v-w] } }}最后返回总数全部代码123456789101112131415func fourSumCount(A []int, B []int, C []int, D []int) int { des := map[int]int{} ans:=0 for _,v :=range A{//遍历两个数组,将两个数组的和作为一个索引,进行+1操作 for _,w:=range B{ des[v+w]++ } } for _,v :=range C{//遍历另两个数组,如果这两个数组进行相加的和的相反数在map中不为1,则证明出现过 for _,w:=range D{ ans +=des[-v-w] } } return ans//返回总数}补充:算法题:三个数相加等于某个特定值题目来自于leetcode第十五题给定一个n个整数的数组S,是否存在S中的元素a,b,c,使得a + b + c = 0? 查找数组中所有唯一的三元组,它们的总和为零。注意:解决方案集不能包含重复的三元组。例子给定数组:1S = [-1, 0, 1, 2, -1, -4]解决方案:1[[-1, 0, 1],[-1, -1, 2]]在刚看到这道题目的题目的时候,首先想到的就是暴力解法,将数组排序后直接嵌套三个循环,这样子虽然简单,但是时间复杂度确实n^3,遇到数据量过大的时候消耗太大,提交的时候并没有通过。自己在想了一段时间后想到了一些优化方案,但是本质上都没有将次方缩减,所以仍然需要改进,目标为n^2。首先,目标为n^2的话,就需要将数组扫描两遍,第一层循环没有问题,但要将第二层和第三层循环缩减为扫描一遍,因为是要将两个数相加等于某个值,所以可将有序数组分别从前往后和从后往前扫描,直至碰头,碰头后如果继续循环的话,所得到的结果会重复,所以到碰头后可以跳出循环。这样子只需要扫描数组一遍就可达到两层循环的结果。思路简单是这样,在实现的时候要考虑一些其他的问题,具体实现的代码如下:12345678910111213141516171819202122232425262728293031public class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> result = new LinkedList<List<Integer>>(); if(nums.length<3){ return result; } Arrays.sort(nums); int left=0,right=nums.length-1; for(int mid=0;mid< nums.length-2;mid++){ if(nums[mid]>0) break; if(mid == 0 || (mid > 0 && nums[mid] != nums[mid-1])){ left=mid+1; right=nums.length-1; while(left<right){ if(nums[left]+nums[mid]+nums[right] ==0){ result.add(Arrays.asList(nums[mid],nums[left],nums[right])); while (left < right && nums[left] == nums[left+1]) left++; while (left < right && nums[right] == nums[right-1]) right--; left++; right--; }else if(nums[left]+nums[mid]+nums[right]<0){ left++; }else if(nums[left]+nums[mid]+nums[right]>0){ right--; } } } } return result; }}
-
看代码吧~123456789//利用正则表达式压缩字符串,去除空格或制表符func compressStr(str string) string { if str == "" { return "" } //匹配一个或多个空白符的正则表达式 reg := regexp.MustCompile("\\s+") return reg.ReplaceAllString(str, "")}补充:go语言去除字符串尾部所有空格刷 leetcode 的一个算法题。要求只删除字符串尾部的所有字符串. google 只搜出通过 strings.Trim() 方法删除前后空格或者删除字符串前边和后边的固定子字符串。方法从字符串后端开始计算空格数量,然后用切片切掉:1234567891011func deleteTailBlank(str string) string { spaceNum := 0 for i := len(str)-1; i >= 0; i-- { // 去除字符串尾部的所有空格 if str[i] == ' ' { spaceNum++ } else { break } } return str[:len(str)-spaceNum]}补充:go:字符串去除空格和换行符 strings.Replace看代码吧~12345678910111213141516package main import ( "fmt" "strings") func main() { str := "这里是 www\n.runoob\n.com" fmt.Println("-------- 原字符串 ----------") fmt.Println(str) // 去除空格 str = strings.Replace(str, " ", "", -1) // 去除换行符 str = strings.Replace(str, "\n", "", -1) fmt.Println("-------- 去除空格与换行后 ----------") fmt.Println(str)}
-
本文是Go比较有名的一个坑,在以前面试的时候也被问过,为什么想起来写这个?因为我们线上就真实出现过这个坑,写给不了解的人在使用 if err != nil 的时候提高警惕。Go语言的interface{}在使用过程中有一个特别坑的特性,当你比较一个interface{}类型的值是否是nil的时候,这是需要特别注意避免的问题。先来看看一个demo:123456789101112131415package mainimport "fmt"type ErrorImpl struct{}func (e *ErrorImpl) Error() string { return ""}var ei *ErrorImplvar e errorfunc ErrorImplFun() error { return ei}func main() { f := ErrorImplFun() fmt.Println(f == nil)}输出:false为什么不是true?想要理解这个问题,首先需要理解interface{}变量的本质。在Go语言中,一个interface{}类型的变量包含了2个指针,一个指针指向值的在编译时确定的类型,另外一个指针指向实际的值。123456789101112131415161718192021222324// InterfaceStructure 定义了一个interface{}的内部结构type InterfaceStructure struct { pt uintptr // 到值类型的指针 pv uintptr // 到值内容的指针}// asInterfaceStructure 将一个interface{}转换为InterfaceStructurefunc asInterfaceStructure(i interface{}) InterfaceStructure { return *(*InterfaceStructure)(unsafe.Pointer(&i))}func main() { var i1, i2 interface{} var v1 int = 23 var v2 int = 23 i1 = v1 i2 = v2 fmt.Printf("sizeof interface{} = %d\n", unsafe.Sizeof(i1)) fmt.Printf("i1 %v %+v\n", i1, asInterfaceStructure(i1)) fmt.Printf("i2 %v %+v\n", i2, asInterfaceStructure(i2)) var nilInterface interface{} var str *string fmt.Printf("nil interface = %+v\n", asInterfaceStructure(nilInterface)) fmt.Printf("nil string = %+v\n", asInterfaceStructure(str)) fmt.Printf("nil = %+v\n", asInterfaceStructure(nil))}输出:sizeof interface{} = 16i1 23 {pt:4812032 pv:825741246928}i2 23 {pt:4812032 pv:825741246936}nil interface = {pt:0 pv:0}nil string = {pt:4802400 pv:0}nil = {pt:0 pv:0}当我们将一个具体类型的值赋值给一个interface{}类型的变量的时候,就同时把类型和值都赋值给了interface{}里的两个指针。如果这个具体类型的值是nil的话,interface{}变量依然会存储对应的类型指针和值指针。如何解决?方法一返回的结果进行非nil检查,然后再赋值给interface{}变量12345678910111213141516type ErrorImpl struct{}func (e *ErrorImpl) Error() string { return ""}var ei *ErrorImplvar e errorfunc ErrorImplFun() error { if ei == nil { return nil } return ei}func main() { f := ErrorImplFun() fmt.Println(f == nil)}输出:true方法二返回具体实现的类型而不是interface{}123456789101112131415package mainimport "fmt"type ErrorImpl struct{}func (e *ErrorImpl) Error() string { return ""}var ei *ErrorImplvar e errorfunc ErrorImplFun() *ErrorImpl { return ei}func main() { f := ErrorImplFun() fmt.Println(f == nil)}输出:true解决由于第三方包带来的坑由于有的error是第三方包返回的,又自己不想改第三方包,只好接收处理的时候想办法。方法一利用interface{}原理1234is:=*(*InterfaceStructure)(unsafe.Pointer(&i))if is.pt==0 && is.pv==0 { //is nil do something}将底层指向值和指向值的类型的指针打印出来如果都是0,表示是nil方法二利用断言,断言出来具体类型再判断非空123456789101112131415161718type ErrorImpl struct{}func (e ErrorImpl) Error() string { return "demo"}var ei *ErrorImplvar e errorfunc ErrorImplFun() error { //ei = &ErrorImpl{} return ei}func main() { f := ErrorImplFun() //当然error实现类型较多的话使用 //switch case方式断言更清晰 res, ok := f.(*ErrorImpl) fmt.Printf("ok:%v,f:%v,res:%v", ok, f == nil, res == nil)}输出:ok:true,f:false,res:true方法三利用反射123456789101112131415type ErrorImpl struct{}func (e ErrorImpl) Error() string { return "demo"}var ei *ErrorImplvar e errorfunc ErrorImplFun() error { //ei = &ErrorImpl{} return ei}func main() { f := ErrorImplFun() rv := reflect.ValueOf(f) fmt.Printf("%v", rv.IsNil())}输出:true注意⚠:断言和反射性能不是特别好,如果不得已再使用,控制使用有助于提升程序性能。由于函数接收类型导致的panic:12345678910111213type ErrorImpl struct{}func (e ErrorImpl) Error() string { return "demo"}var ei *ErrorImplvar e errorfunc ErrorImplFun() error { return ei}func main() { f := ErrorImplFun() fmt.Printf(f.Error())}输出:panic: value method main.ErrorImpl.Error called using nil *ErrorImpl pointer解决:123func (e *ErrorImpl) Error() string { return "demo"}输出:demo可以发现将接收类型变成指针类型就可以了。以上就是 nil 相关的坑,希望大家可以牢记,如果 ”幸运“ 的遇到了,可以想到这些可能性。补充:go 语言 interface{} 的易错点如果说 goroutine 和 channel 是 go 语言并发的两大基石,那 interface 就是 go 语言类型抽象的关键。在实际项目中,几乎所有的数据结构最底层都是接口类型。说起 C++ 语言,我们立即能想到是三个名词:封装、继承、多态。go 语言虽然没有严格意义上的对象,但通过 interface,可以说是实现了多态性。(由以组合结构体实现了封装、继承的特性)123456789101112131415161718192021package maintype animal interface { Move()}type bird struct{}func (self *bird) Move() { println("bird move")}type beast struct{}func (self *beast) Move() { println("beast move")}func animalMove(v animal) { v.Move()}func main() { var a *bird var b *beast animalMove(a) // bird move animalMove(b) // beast move}go 语言中支持将 method、struct、struct 中成员定义为 interface 类型,使用 struct 举一个简单的栗子使用 go 语言的 interface 特性,就能实现多态性,进行泛型编程。二,interface 原理如果没有充分了解 interface 的本质,就直接使用,那最终肯定会踩到很深的坑,要用就先要了解,先来看看 interface 源码1234567891011121314151617type eface struct { _type *_type data unsafe.Pointer} type _type struct { size uintptr // type size ptrdata uintptr // size of memory prefix holding all pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldalign uint8 // alignment of struct field with this type kind uint8 // enumeration for C alg *typeAlg // algorithm table gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero}可以看到 interface 变量之所以可以接收任何类型变量,是因为其本质是一个对象,并记录其类型和数据块的指针。(其实 interface 的源码还包含函数结构和内存分布,由于不是本文重点,有兴趣的同学可以自行了解)三,interface 判空的坑对于一个空对象,我们往往通过 if v == nil 的条件语句判断其是否为空,但在代码中充斥着 interface 类型的情况下,很多时候判空都并不是我们想要的结果(其实了解或聪明的同学从上述 interface 的本质是对象已经知道我想要说的是什么)123456789101112131415161718192021222324package main type animal interface { Move() } type bird struct{} func (self *bird) Move() { println("bird move") } type beast struct{} func (self *beast) Move() { println("beast move") } func animalMove(v animal) { if v == nil { println("nil animal") } v.Move() } func main() { var a *bird // nil var b *beast // nil animalMove(a) // bird move animalMove(b) // beast move }还是刚才的栗子,其实在 go 语言中 var a *bird 这种写法,a 只是声明了其类型,但并没有申请一块空间,所以这时候 a 本质还是指向空指针,但我们在 aminalMove 函数进行判空是失败的,并且下面的 v.Move() 的调用也是成功的,本质的原因就是因为 interface 是一个对象,在进行函数调用的时候,就会将 bird 类型的空指针进行隐式转换,转换成实例的 interface animal 对象,所以这时候 v 其实并不是空,而是其 data 变量指向了空。这时候看着执行都正常,那什么情况下坑才会绊倒我们呢?只需要加一段代码12345678910111213141516171819202122232425262728package main type animal interface { Move() } type bird struct { name string } func (self *bird) Move() { println("bird move %s", self.name) // panic } type beast struct { name string } func (self *beast) Move() { println("beast move %s", self.name) // panic } func animalMove(v animal) { if v == nil { println("nil animal") } v.Move() } func main() { var a *bird // nil var b *beast // nil animalMove(a) // panic animalMove(b) // panic }在代码中,我们给派生类添加 name 变量,并在函数的实现中进行调用,就会发生 panic,这时候的 self 其实是 nil 指针。所以这里坑就出来了。有些人觉得这类错误谨慎一些还是可以避免的,那是因为我们是正向思维去代入接口,但如果反向编程就容易造成很难发现的 bug123456789101112131415161718192021222324252627282930313233343536package main type animal interface { Move() } type bird struct { name string } func (self *bird) Move() { println("bird move %s", self.name) } type beast struct { name string } func (self *beast) Move() { println("beast move %s", self.name) } func animalMove(v animal) { if v == nil { println("nil animal") } v.Move() } func getBirdAnimal(name string) *bird { if name != "" { return &bird{name: name} } return nil } func main() { var a animal var b animal a = getBirdAnimal("big bird") b = getBirdAnimal("") // return interface{data:nil} animalMove(a) // bird move big bird animalMove(b) // panic }这里我们看到通过函数返回实例类型指针,当返回 nil 时,因为接收的变量为接口类型,所以进行了隐性转换再次导致了 panic(这类反向转换很难发现)。那我们如何处理上述这类问题呢。我这边整理了三个点1,充分了解 interface 原理,使用过程中需要谨慎小心2,谨慎使用泛型编程,接收变量使用接口类型,也需要保证接口返回为接口类型,而不应该是实例类型3,判空是使用反射 typeOf 和 valueOf 转换成实例对象后再进行判空
-
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第三期2026/08/21 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;念擎-华为云AI开发者运营案例开发专家
本期直播内容:AI六层能力首次详细解读 + 新一代华为云开发者空间亮相 + 校园案例直播带练
即将直播
热门标签