Golang 教程

go语言&protobuf3 专栏收录该内容
9 篇文章 0 订阅

特点

Go语言是一门强类型的通用编程语言。它的基础语法与C语言很类似,但对于变量的声明有所不同,也对其他的一些优秀编程语言有所借鉴。另外,Go语言支持垃圾回收。

与C++相比,Go语言并不包括如异常处理、继承、泛型、断言、虚函数等功能,但增加了 Slice 型、并发、管道、垃圾回收、接口(Interface)等特性的语言级支持。

Go语言对并发编程的支持是天生的、自然的和高效的。Go语言为此专门创造出了一个关键字“go”。使用这个关键字,我们就可以很容易的使一个函数被并发的执行。

撰写风格

在Go语言中有几项规定,而且这些是强制的,当不匹配以下规定时编译将会产生错误。

1.每行程序结束后不需要撰写分号;

2.大括号{不能够换行放置。

3.if 判断式和 for 循环不需要以小括号包覆起来。

代码示例

所有语言的第一个程序都是“hello world”,我们也不例外,在这里用Go语言写一个“hello world”程序。

package main import "fmt" func main(){ fmt.Printf("Hello World!"); }

  •  

这段代码运行,会在控制台下打印出“hello world!”。

如果你把fmt.Printf("Hello World!")改为fmt.Println("Hello World!"),在打印“Hello World”后换行。

还有值得注意的一点是:Golang官方的约定是每行程序结束后不需要撰写分号;,但是如果你不小心加了,编译器也不会报错,笔者的编译器是go1.9.2 windows/amd64。

 

接下来我们看一个简单的程序,可以打印出当前运行环境中的Go语言版本号。

package main import ( "fmt" "runtime" ) func main() { fmt.Println(runtime.Version()) }

 

标识符

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )

 

语句的结尾

Go语言中是不需要类似于Java需要冒号结尾,默认一行就是一条数据

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分

 

import

import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包的函数,fmt 包实现了格式化 IO(输入/输出)的函数

可以是相对路径也可以是绝对路径,推荐使用绝对路径(起始于工程根目录)

 

1. 点操作 我们有时候会看到如下的方式导入包

import( . "fmt" )

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调

用的fmt.Println("hello world")可以省略的写成Println("hello world")

 

2. 别名操作 别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

import( f "fmt" )

别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world")

 

3. _操作 这个操作经常是让很多人费解的一个操作符,请看下面这个import

import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数

 

初始化顺序:

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

 

main与init

  • 这两个函数在定义时不能有任何的参数和返回值
  • 虽然一个package里面可以写任意多个init函数,但推荐只用一个
  • Go程序会自动调用init()和main()
  • 每个package中的init函数都是可选的,但package main就必须包含一个main函数
  • 先调用init函数,再调用main函数
  • 运行程序,必须要运行存在main函数的go文件

 

数据类型

Go语言按类别有以下几种数据类型:

1.bool,一个字节,值是true或者false,不可以用0或者1表示(java中boolean占用4个字节,而boolean作为数组出现时,每个boolean占用1个字节)

2.int/uint(带符号为与不带符号位的int类型):根据平台不同是32位或者64位

3.intx/uintx:x代表任意位数,例如:int3,代表占3bit的int类型

4.byte占用8位,一个字节,相当于uint8,不带符号位

5.floatx:由于没有double类型,所以float64就是double。float32小数精确到7位,float64小数精确到15位。

6.complex64/complex128:复数类型

7.uintptr:保存指针用的类型,也是随着平台改变而改变,因为指针的长度就是随平台而变。

8.其他类型值:array,struct,string

9.引用类型:slice,map,channel

10.接口类型:interface

11.函数类型:func

注意:Golang中还有个rune类型,它等价于int32类型。

类型转换

Golang是静态类型的编程语言,所有数据的类型在编译期确定了。而且 Golang中即使是底层存的是一个类型,声明的类型不一样,也要强制转换才能互用。

在Go 语言里面没有隐式转换,遇到不同类型想互用,只能进行强制类型转换。

Go语言类型转换基本格式如下:

type_name(expression)

  • 1

其中type_name是数据类型,expression是原始数据或表达式。

以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:

package main import "fmt" func main() { var sum int = 17 var count int = 5 var mean float32 mean = float32(sum)/float32(count) //强制转换 fmt.Printf("mean 的值为: %f\n",mean) }

 

利用 unsafe 包进行指针类型转换(https://studygolang.com/articles/1414 https://studygolang.com/articles/9446 )

unsafe.Pointer其实就是类似C的void *,在golang中是用于各种指针相互转换的桥梁。uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换。uintptr和unsafe.Pointer的区别就是:unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象,uintptr类型的目标会被回收。golang的unsafe包很强大,基本上很少会去用它。它可以像C一样去操作内存,但由于golang不支持直接进行指针运算,所以用起来稍显麻烦。

 

C 的一些类型转换为GO的类型,cgo内置了一些函数: https://golang.org/src/cmd/cgo/doc.go

例如: char* 转为 go 的string C.GoString(char*)

 

变量命名原则

Go语言变量名由字母、数字、下划线组成,其中首个字母不能为数字。

但是字母不限于英文字母,所有UTF-8字符都可以。

 

声明和初始化方式

1.使用var关键字

声明和初始化变量的一般形式是使用 var 关键字,例如:

var a int = 9

注意,Go语言变量类型放在变量名之后!

如果上面的代码段没有初始化值,那么变量就会被初始化为对应类型的零值。

var a int

这个代码片段声明了一个int类型变量a,并把a初始化为int零值,即0。

2.忽略类型

这种声明和初始化变量的方式相比于第一种,就是省去了变量的类型,Go语言编译器可以根据你的初始值自动判断出类型。例如:

var a = 5

这里面的a被自动识别为int类型。

3.直接初始化 (该方法定义的变量是局部变量) [golang 易犯错误] golang 局部变量初始化:=的陷阱

这种方法定义和初始化变量,既不需要var关键字,也不需要指定类型,快捷又方便,实际的代码中建议用这种方式。

a := 3

以上代码段定义了一个变量a,并把它初始化为3。

4.多变量声明

Go语言可以同时声明和初始化多个变量这个和Python有点像,具体语法如下:

//类型相同多个变量, 非全局变量 var vname1, vname2, vname3 type vname1, vname2, vname3 = v1, v2, v3 var vname1, vname2, vname3 = v1, v2, v3 //和python很像,不需要显示声明类型,自动推断 vname1, vname2, vname3 := v1, v2, v3 //出现在:=左侧的变量不应该是已经被声明过的,且只能在函数体内出现

 

5. 空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃

_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值

 

6. 常量声明

常量是一个简单值的标识符,在程序运行时,不会被修改的量。

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

不曾使用的常量,在编译的时候,是不会报错的

显示指定类型的时候,必须确保常量左右值类型一致,需要时可做显示类型转换。这与变量就不一样了,变量是可以是不同的类型值

const identifier [type] = value

显式类型定义: const b string = "abc" 隐式类型定义: const b = "abc"

package main import "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) }

结果

面积为 : 50 1 false str

 

常量可以作为枚举

const ( Unknown = 0 Female = 1 Male = 2 )

常量组中如不指定类型和初始化值,则与上一行非空常量右值相同

package main import ( "fmt" ) func main() { const ( x uint16 = 16 y s = "abc" z ) fmt.Printf("%T,%v\n", y, y) fmt.Printf("%T,%v\n", z, z) }

结果

uint16,16 string,abc

 

iota,特殊常量,可以认为是一个可以被编译器修改的常量

在每一个const关键字出现时,被重置为0,然后再下一个const出现之前,每出现一次iota,其所代表的数字会自动增加1

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 main import "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自增,则必须显式恢复。且后续自增值按行序递增

自增默认是int类型,可以自行进行显示指定类型

数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址

  •  

代码示例

把上面几种变量定义和初始化的方法整理一下,如下:

package main import ( "fmt" ) var c, d int = 1, 2 var e, f = 123, "hello" //!!注意:下行这种不带声明格式的只能在函数体中出现 //g, h := 123, "hello" func main() { var v21 int32 //被定义初始化为0 var v22 int = 2 var v23 = 3 //被自动识别为int类型 v24 := 4 //简易声明&定义的方式 v21 = int32(v23) //强制转换 g, h := 123, "hello" fmt.Println("v21 is", v21) //v21被赋新值 fmt.Println("v22 is", v22) fmt.Println("v23 is", v23) fmt.Println("v24 is", v24) fmt.Println(c, d, e, f, g, h) }

 

if语句

if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。

if condition { // do something }

 

if-else结构定义

如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if 和 else 后的两个代码块是相互独立的分支,只可能执行其中一个。

if condition { // do something } else { // do something }

多分支if-else结构定义

如果存在第三个分支,则可以使用下面这种三个独立分支的形式:

if condition1 { // do something } else if condition2 { // do something else }else { // catch-all or default }

else-if分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else-if 结构。如果你必须使用这种形式,则把尽可能先满足的条件放在前面。

即使当代码块之间只有一条语句时,大括号也不可被省略(尽管有些人并不赞成,但这还是符合了软件工程原则的主流做法)。

格式规则

Go语言中条件语句不需要圆括号。但是分支中必须有大括号。

Go语言条件语句中可以用:=初始化一个变量,不过需要记住,这个变量是局部变量,该变量的作用域仅在if范围之内。

Go语言里面对if/else格式对齐要求很严格,如果需要if/else组合,则需要在if语句结束的大括号后面就跟上else。

关键字 if 和 else 之后的左大括号{必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号}必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。

例如,下面的if-else代码段是非法的:

if x{ } else { // 非法!!! }

代码示例

我们使用if来完成一个简单的判断程序。

package main import "fmt" func main() { // 基本的例子 if 7%2 == 0 { fmt.Println("7 is even") } else { fmt.Println("7 is odd") } // 只有if条件的情况 if 8%4 == 0 { fmt.Println("8 is divisible by 4") } // if条件可以包含一个初始化表达式,这个表达式中的变量 // 是这个条件判断结构的局部变量 if num := 9; num < 0 { fmt.Println(num, "is negative") } else if num < 10 { fmt.Println(num, "has 1 digit") } else { fmt.Println(num, "has multiple digits") } }

 

for结构介绍

Go语言只有for循环这一种循环结构。

基本的for循环包含三个由分号分开的组成部分:

1.初始化语句:在第一次循环执行前被执行

2.循环条件表达式:每轮迭代开始前被求值

3.后置语句:每轮迭代后被执行

初始化语句一般是一个短变量声明,这里声明的变量仅在整个 for 循环语句可见。

如果条件表达式的值变为 false,那么迭代将终止。

注意:不像 C,Java,或者 Javascript 等其他语言,Go语言中的for循环语句的三个部分不需要用括号括起来,但循环体必须用 { } 括起来。

基于计数器的for循环

基于计数器的迭代,基本形式为:

for 初始化语句; 条件语句; 修饰语句 { //循环语句 }

 

我们用for循环来计算10以内正整数的和。

package main import( "fmt" ) func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) }

 

由花括号括起来的代码块会被重复执行已知次数,该次数是根据计数器(此例为 i)决定的。循环开始前,会执行且仅会执行一次初始化语句 i := 0;,这比在循环之前声明更为简短。紧接着的是条件语句 i < 10;,在每次循环开始前都会进行判断,一旦判断结果为 false,则退出循环体。最后一部分为修饰语句 i++,一般用于增加或减少计数器。

这三部分组成的循环的头部,它们之间使用分号 ; 相隔,但并不需要括号 () 将它们括起来。例如:

for (i = 0; i < 10; i++) { } //非法代码!

 

所示是非法的代码!

和if-else语句类似,for循环中左花括号{必须和 for 语句在同一行,计数器的生命周期在遇到右花括号} 时便终止。一般习惯使用 i、j、z 或 ix 等较短的名称命名计数器。

基于条件判断的for循环

for 循环的第二种形式是没有头部的条件判断(类似其它语言中的 while 循环),基本形式为:

for 条件语句 {}

 

也可以认为这是没有初始化语句和修饰语句的 for 结构,因此 ;;便是多余的了。

我们来写一个基于条件判断的简单for循环,示例代码如下:

package main import "fmt" func main() { var i int = 5 for i >= 0 { i = i - 1 fmt.Printf("The variable i is now: %d\n", i) } }

 

for-range结构

提到for循环,我们必须再提一下range()这个内置函数,这是 Go 语言特有的一种的迭代结构,它一般用来遍历数组,slice和map等集合。我们用range来遍历一个数组,以下代码会打印出每个值的序号和对应的值。

package main import( "fmt" ) func main() { arr := [...]int{6, 7, 8} for i, v := range arr { fmt.Println(i,v) } }

输出:

0 6 1 7 2 8

 

无限循环

Go 语言有以下几种无限循环:

i:=0; ; i++ for { } for ;; { } for true { }

 

这些无限循环都是 for 循环的条件语句被省略了。如果 for 循环的头部没有条件语句,那么就会认为条件永远为 true。因此如果不想造成死循环,循环体内必须有相关的条件判断以确保会在某个时刻退出循环。

跳出循环的语句

 

break:跳出循环体

continue:跳出一次循环

goto:可以无条件地转移到过程中指定的行

package main import "fmt" func main() { /* 定义局部变量 */ var a int = 10 /* 循环 */ LOOP: for a < 20 { if a == 15 { /* 跳过迭代 */ a = a + 1 goto LOOP } fmt.Printf("a的值为 : %d\n", a) a++ } }

 

Switch结构

相比较 C 和 Java 等其它语言而言,Go 语言中的 switch 结构使用上更加灵活。它接受任意形式的表达式,例如:

switch var1 { case val1: ... case val2: ... default: ... }

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号{必须和 switch 关键字在同一行。

您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。

每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。一旦成功地匹配到每个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说你不需要特别使用 break 语句来表示结束。

Go语言里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch。

 

含初始化语句的switch

 

switch 语句的另外一种形式是包含初始化的语句,例如:

switch initialization { case val1: ... case val2: ... default: ... }

 

这种形式可以非常优雅地进行条件判断:

switch result := calculate(); { case result < 0: ... case result > 0: ... default: // 0 }

 

Type Switch

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

switch x.(type){ case type: statement(s); case type: statement(s); /* 你可以定义任意个数的case */ default: /* 可选 */ statement(s); }

package main import "fmt" func main() { var x interface{} switch i := x.(type) { case nil: fmt.Printf(" x 的类型 :%T",i) case int: fmt.Printf("x 是 int 型") case float64: fmt.Printf("x 是 float64 型") case func(int) float64: fmt.Printf("x 是 func(int) 型") case bool, string: fmt.Printf("x 是 bool 或 string 型" ) default: fmt.Printf("未知型") } }

结果

x 的类型 :<nil>

fallthrough关键字

switch语句中,如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用fallthrough关键字来达到目的。例如:

package main import( "fmt" ) func main() { i :=2 switch i { case 0: fmt.Printf("0") case 1: fmt.Printf("1") case 2: fallthrough //fallthrough会强制执行后面的case代码 case 3: fmt.Printf("3") default: fmt.Printf("Default") } }

 

以上代码在 i=0时输出0,i=1时输出1,i=2时输出3,i=3时输出3,其他时候便输出Default。

 

select 语句(具体使用需要扩展一下)

select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select {

case e1 := <-ch1:

//如果ch1通道成功读取数据,则执行该case处理语句

fmt.Printf("1th case is selected. e1=%v", e1)

case e2 := <-ch2:

//如果ch2通道成功读取数据,则执行该case处理语句

fmt.Printf("2th case is selected. e2=%v", e2)

default:

//如果上面case都没有成功,则进入default处理流程

fmt.Println("default!.")

}

 

package main import "fmt" func main() { var c1, c2, c3 chan int var i1, i2 int select { case i1 = <-c1: fmt.Printf("received ", i1, " from c1\n") case c2 <- i2: fmt.Printf("sent ", i2, " to c2\n") case i3, ok := (<-c3): // same as: i3, ok := <-c3 if ok { fmt.Printf("received ", i3, " from c3\n") } else { fmt.Printf("c3 is closed\n") } default: fmt.Printf("no communication\n") } }

 

综合代码示例

这里我们把上面所讲的知识点串联起来,展示下 Go 语言中变化多样的switch语句,如下:

package main import( "fmt" ) func main() { /* local variable definition */ Num := 6 var grade string = "B" var marks int = 90 switch { case 0 <= Num && Num <= 3: fmt.Println("0-3") case 4 <= Num && Num <= 6: fmt.Println("4-6") case 7 <= Num && Num <= 9: fmt.Println("7-9") } switch marks { case 90: grade = "A" case 80: grade = "B" case 50,60,70 : grade = "C" default: grade = "D" } switch { case grade == "A" : fmt.Println("Excellent!" ) case grade == "B", grade == "C" : fmt.Println("Well done" ) case grade == "D" : fmt.Println("You passed" ) case grade == "F": fmt.Println("Better try again" ) default: fmt.Println("Invalid grade" ); } fmt.Println("Your grade is :", grade ); }

以上代码的打印结果是:

4-6 Excellent! Your grade is : A

 

数组定义

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。需要强调:数组长度也是数组类型的一部分。

Go语言数组

数组是Go语言编程中最常用的数据结构之一。顾名思义,数组就是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素( element),一个数组包含的元素个数被称为数组的长度。需要强调的一点是Go语言中数组的长度固定,无法扩容。

声明定义

数组在定义初始化的时候,长度可以指定,也可以不指定让编译器自动推断。几种常见的数组定义初始化的方式如下:

var a [3] int //3个int型的数组,初始值是3个0,数组“零值”状态 arr:=[5]int{1,2,3,4,5} //定义并初始化长度为5的数组 var array2 = [...]int{6, 7, 8} //不声明长度 q := [...] int {1,2,3} //不声明长度 r := [...] int {99:-1} //长度为100的数组,只有最后一个是-1,其他都是0

 

在声明var arr1 [5]int中每个元素是一个整形值,当声明数组时所有的元素都会被自动初始化为默认值 0。

 

多维数组

var threedim [5][10][4]int

三维数组

a = [3][4]int{ {0, 1, 2, 3} , /* 第一行索引为 0 */ {4, 5, 6, 7} , /* 第二行索引为 1 */ {8, 9, 10, 11} /* 第三行索引为 2 */ }

int val = a[2][3]

 

数组元素访问

Go语言中,可以使用数组下标来访问数组中的元素。数组下标从0开始,len(arr)-1则表示最后一个元素的下标。

只有有效的索引可以被使用,当使用等于或者大于 len(arr)的索引时:如果编译器可以检测到,会给出索引超限的提示信息;如果检测不到的话编译会通过而运行时会 panic。

比起C语言,Go语言可以直接调用打印函数打印出整个数组。下面我们定义一个二维数组,然后来打印出数组长度和整个数组:

package main import "fmt" func main() { myArray := [3][4]int{{1,2,3,4},{1,2,3,4},{1,2,3,4}} //打印一维数组长度 fmt.Println(len(myArray)) //打印二维数组长度 fmt.Println(len(myArray[1])) //打印整个二维数组 fmt.Println(myArray) }

 

数组指针访问和操作数组元素:(*pArray)[index]

 

for-range结构

这是Go语言一种独有的结构,可以用来遍历访问数组的元素。

for ix, value := range array01 { ... }

第一个返回值 ix 是数组的索引,第二个是在该索引位置的值;他们都是仅在 for 循环内部可见的局部变量。value 只是 array01 某个索引位置的值的一个拷贝,不能用来修改 array01 该索引位置的值。例如:

package main import "fmt" func main() { var arr1 [5]int for i:=0; i < len(arr1); i++ { arr1[i] = i * 2 } for i:=0; i < len(arr1); i++ { fmt.Printf("Array at index %d is %d\n", i, arr1[i]) }

 

当然,如果你在遍历数组元素的时候,如果想遗弃索引id,可以直接把索引id标为下划线_。一个求数组里面的平均值的例子如下:

package main import ( "fmt" ) func main() { sum := 0.0 var avg float64 xs := [6]float64{1, 2, 3, 4, 5, 6} switch len(xs) { case 0: avg = 0 default: for _, v := range xs {//下划线表示那个值舍去,即舍去下标索引 sum += v } avg = sum / float64(len(xs)) } fmt.Println(avg) }

 

字符串介绍

几乎任何程序都离不开字符串,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。

Go语言字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是字节的定长数组。

Go 代码使用 UTF-8 编码(且不能带 BOM),同时标识符支持 Unicode 字符。在标准库 unicode 包及其子包 utf8、utf16中,提供了对 Unicode 相关编码、解码的支持,同时提供了测试 Unicode 码点(Unicode code points)属性的功能。

Go语言中的字符串也可能根据需要占用 1至 4 个字节,这与其它语言如 C++、Java 或者 Python 不同(Java 始终使用 2 个字节)。Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。

Go语言支持以下2种形式的字符串:

1. 解释性字符串:带引号的字节序列。该类字符串使用双引号括起来,其中的相关的转义字符将被替换。例如:

str := "laoYu"

2. 原生字符串: 该类字符串使用反引号(注意:不是单引号)括起来,支持换行。例如:

`This is a raw string \n`

上面原生字符串中的 \n 会被原样输出。

获取字符串长度可以用内置的函数len。

字符串函数包

strings 包提供了很多操作字符串的简单函数,通常一般的字符串操作需求都可以在这个包中找到。

下面简单举几个例子:

  • 判断是否以某字符串打头/结尾

strings.HasPrefix(s, prefix string) bool

strings.HasSuffix(s string, suffix string) bool

  • 字符串分割

strings.Split(s string, sep string) []string

  • 返回子串索引

strings.Index(s string, sub string) int

strings.LastIndex 最后一个匹配索引

  • 字符串连接

strings.Join(a []string, sep string) string

另外可以直接使用“+”来连接两个字符串

  • 字符串替换

strings.Replace(s, old, new string, n int) string

  • 字符串转化为大小写

strings.ToUpper(s string) string

strings.ToLower(s string) string

  • 统计某个字符在字符串出现的次数

strings.Count(s string, sep string) int

  • 判断字符串的包含关系

strings.Contains(s, substr string) bool

strconv 包提供了基本数据类型和字符串之间的转换。在 Go 中,没有隐式类型转换,一般的类型转换可以这么做:int32(i),将 i (比如为 int 类型)转换为 int32,然而,字符串类型和 int、float、bool 等类型之间的转换却没有这么简单。

与字符串相关的类型转换都是通过 strconv 包实现的。

针对从数字类型转换到字符串,Go 提供了以下函数:

- strconv.Itoa(i int) string

返回数字 i 所表示的字符串类型的十进制数。

代码示例

下面我们示范下上述函数的简单使用:

package main import( "fmt" "strings" "strconv" ) func main() { str01 :=`This is a raw string \n` //原生字符串 str02 :="This is a raw string \n"//引用字符串 fmt.Println("原生字符串和引用字符串的区别") fmt.Printf(str01) fmt.Println("") fmt.Printf(str02) fmt.Println("") fmt.Println("+连接字符串") var str03 string = str01 +str02 fmt.Printf(str03) fmt.Println("") var str string = "This is an example of a string" fmt.Println("HasPrefix 函数的用法") fmt.Printf("T/F? Does the string \"%s\"have prefix %s? ", str, "Th") //前缀 fmt.Printf("%t\n", strings.HasPrefix(str, "Th\n")) fmt.Println("") fmt.Println("Contains 函数的用法") fmt.Println(strings.Contains("seafood", "foo")) //true fmt.Println(strings.Contains("seafood", "bar")) //false fmt.Println("Count 函数的用法") fmt.Println(strings.Count("cheese", "e")) // 3 fmt.Println(strings.Count("five", "")) fmt.Println("") fmt.Println("Index 函数的用法") fmt.Println(strings.IndexRune("NLT_abc", 'b')) // 返回第一个匹配字符的位置,这里是4 fmt.Println(strings.IndexRune("NLT_abc", 's')) // 在存在返回 -1 fmt.Println(strings.IndexRune("我是中国人", '中')) // 在存在返回 6 fmt.Println("") fmt.Println("Join 函数的用法") s := []string{"foo", "bar", "baz"} fmt.Println(strings.Join(s, ", ")) // 返回字符串:foo, bar, baz fmt.Println("") fmt.Println("LastIndex 函数的用法") fmt.Println(strings.LastIndex("go gopher", "go")) // 3 fmt.Println("") fmt.Println("Replace 函数的用法") fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) fmt.Println("") fmt.Println("Split 函数的用法") fmt.Printf("%q\n", strings.Split("a,b,c", ",")) fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) fmt.Printf("%q\n", strings.Split(" xyz ", "")) fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) fmt.Println("") fmt.Println("ToLower 函数的用法") fmt.Println(strings.ToLower("Gopher")) //gopher fmt.Println("") fmt.Println("strconv.Itoa()函数用法") var an int = 6 newS := strconv.Itoa(an) fmt.Printf("The new string is: %s\n", newS) }

字符串和字节数组

字符串的本质就是一个字节数组。在Go 语言里面字符串和字节数组可以相互进行显式转换,这一性质通常被用来“修改”字符串的内容。

因为Go 语言中的字符串是不可变的,也就是说 str[index] 这样的表达式是不可以被放在等号左侧的。如果尝试运行 str[i] = 'D'会得到错误:cannot assign to str[i]。

因此必须先将字符串转换成字节数组,然后再通过修改数组中的元素值来达到修改字符串的目的,最后将字节数组转换回字符串格式。示例代码如下:

package main import ( "fmt" ) var name string //声明一个字符串 var emptyname string = "" //声明一个空字符串 func main() { //申明多个字符串并且赋值 a, b, v := "hello", "word", "widuu" fmt.Println(a, b, v) c := []byte(a) //转换字符串的内容,先转换a的类型为[]byte c[0] = 'n' //赋值 //在转换成字符串类型,其实我们发现我们的a并没有改变 //而是一个新的字符串的改变 d := string(c) //转换为字符串 fmt.Println(a) //hello fmt.Println(c) //[110 101 108 108 111] fmt.Println(d) //nello }

字符串的utf-8特性

UTF-8是被广泛使用的编码格式,是文本文件的标准编码。在 Go语言字符串是UTF-8字符的一个序列。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节。Go 语言这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用UTF-8字符集的文本进行编码和解码。

接下来我们创建一个用于统计字节和字符(rune)的程序,并对字符串 asSASA ddd dsjkdsjs dk 进行分析,然后再分析 asSASA ddd dsjkdsjsこん dk。如下:

package main import ( "fmt" "unicode/utf8" ) func main() { // count number of characters: str1 := "asSASA ddd dsjkdsjs dk" fmt.Printf("The number of bytes in string str1 is %d\n",len(str1)) fmt.Printf("The number of characters in string str1 is %d\n",utf8.RuneCountInString(str1)) str2 := "asSASA ddd dsjkdsjsこん dk" fmt.Printf("The number of bytes in string str2 is %d\n",len(str2)) fmt.Printf("The number of characters in string str2 is %d",utf8.RuneCountInString(str2)) } /* Output: The number of bytes in string str1 is 22 The number of characters in string str1 is 22 The number of bytes in string str2 is 28 The number of characters in string str2 is 24 */

使用buffer高效拼接字符串

我们在之前的关卡讲过,Go语言中字符串的拼接用+。例如:

package main import ( "fmt" ) func main() { //申明多个字符串并且赋值 a, b:= "hello", "world" var c string = a+b fmt.Println(c) //helloworld }

不过用+这种合并方式效率非常低,每合并一次,都是创建一个新的字符串,就必须遍历复制一次字符串。

Java中提供StringBuilder类(最高效,线程不安全)来解决这个问题。Go中也有类似的机制,那就是Buffer(线程不安全)。代码如下:

package main import ( "bytes" "fmt" ) func main() { var buffer bytes.Buffer for i := 0; i < 100; i++ { buffer.WriteString("a") } fmt.Println(buffer.String()) }

使用bytes.Buffer来组装字符串,不需要复制,只需要将添加的字符串放在缓存末尾即可。不过需要强调,Golang源码对于Buffer的定义中并没有任何关于锁的字段,所以Buffer是线程不安全的

 

结构体简介

Go 语言通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new 函数来创建。

组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。

结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type)。在 C++ 它也存在,并且名字也是 struct,在面向对象的编程语言中,跟一个无方法的轻量级类一样。因为 Go 语言中没有类的概念,所以在 Go 中结构体有着更为重要的地位。

 

结构体定义

结构体定义的一般方式如下:

type identifier struct { field1 type1 field2 type2 ... }

 

在结构体内部定义它们的成员变量和类型,只是类型要放到后面,并且变量之间不用逗号。

例如:

type Student struct { name string age int weight float32 score []int }

 

如果成员变量的类型相同的话,可以把它们写到同一行。所以type T struct {a, b int}也是合法的语法,它更适用于简单的结构体。

结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。

 

结构体初始化

这里有两种方式可以初始化结构体,按原始字段顺序进行初始化和自定义字段顺序进行初始化。

在Go语言里可以直接使用 fmt.Println 打印一个结构体,默认输出可以很好的显示它的内容,类似使用 %v 选项。

就像在面向对象语言所作的那样,可以使用点号符给字段赋值:

structname.fieldname = value

同样的,使用点号符可以获取结构体字段的值:

structname.fieldname

示例代码如下:

package main import ( "fmt" ) type Student struct { name string age int weight float32 score []int } func main() { stu01 := Student{"stu01", 23, 55.5, []int{95, 96, 98}} //按照字段顺序进行初始化 stu02 := Student{age: 23, weight: 55.5, score: []int{97, 98}, name: "stu02"} ///通过 field:value 形式初始化,该方式可以自定义初始化字段的顺序 stu01.age = 25 fmt.Println(stu01.age) fmt.Println(stu02) }

 

结构体new函数

关于结构体和指针的结合我们在下一关重点讲下,这里简单提下它们的组合使用。

一般在进行例如type T struct {a, b int}的结构体定义之后

习惯使用t := new(T)给该结构体变量分配内存,它返回指向已分配内存的指针。变量 t是一个指向T的指针,此时结构体字段的值是它们所属类型的零值。

声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t是类型T。在这两种方式中,t 通常被称做类型T的一个实例(instance)或对象(Object)。var t *T = new(T)等价于t := new(T)。

一个简单的结构体new函数的例子如下:

package main import "fmt" type struct1 struct { i1 int f1 float32 str string } func main() { ms := new(struct1) ms.i1 = 10 ms.f1 = 15.5 ms.str= "Chris" fmt.Printf("The int is: %d\n", ms.i1) fmt.Printf("The float is: %f\n", ms.f1) fmt.Printf("The string is: %s\n", ms.str) fmt.Println(ms) }

 

结构体的匿名字段

在类型中,使用不写字段名的方式,使用另一个类型

type Human struct { name string age int weight int } type Student struct { Human // 匿名字段,那么默认Student就包含了Human的所有字段 speciality string } func main() { // 我们初始化一个学生 mark := Student{Human{"Mark", 25, 120}, "Computer Science"} // 我们访问相应的字段 fmt.Println("His name is ", mark.name) fmt.Println("His age is ", mark.age) fmt.Println("His weight is ", mark.weight) fmt.Println("His speciality is ", mark.speciality) // 修改对应的备注信息 mark.speciality = "AI" fmt.Println("Mark changed his speciality") fmt.Println("His speciality is ", mark.speciality) // 修改他的年龄信息 fmt.Println("Mark become old") mark.age = 46 fmt.Println("His age is", mark.age) // 修改他的体重信息 fmt.Println("Mark is not an athlet anymore") mark.weight += 60 fmt.Println("His weight is", mark.weight) }

 

结构体指针

var struct_pointer *Books

以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前

struct_pointer = &Book1;

使用结构体指针访问结构体成员,使用 "." 操作符

struct_pointer.title;

 

结构体实例化也可以是这样的

package main import "fmt" type Books struct { } func (s Books) String() string { return "data" } func main() { fmt.Printf("%v\n", Books{}) }

 

函数

和所有的编程语言一样,Go语言支持各种风格的函数。在Go语言中,当函数执行到代码块最后一行}之前或者return语句的时候会退出,其中 return 语句可以带有零个或多个参数;这些参数将作为返回值供调用者使用。简单的 return 语句也可以用来结束 for 死循环,或者结束一个Go协程(goroutine)。

 

定义语法

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { //这里是处理逻辑代码 //返回多个值 return value1, value2 }

  • func:函数由 func 开始声明
  • funcName:函数名称,函数名和参数列表一起构成了函数签名。
  • input1 type1, input2 type2:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • output1 type1, output2 type2:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 上面返回值声明了两个变量output1和output2,如果你不想声明也可以,直接就两个类型。
  • 如果只有一个返回值且不声明返回值变量,那么你可以省略包括返回值的括号(即一个返回值可以不声明返回类型)
  • 函数体:函数定义的代码集合。

 

这里需要强调的是,Go语言函数的返回值类型和变量定义的数据类型一样,都要遵循Go语言的“后置原则”放在后面,这一点和C语言 函数定义有显著不同。另外,Go语言函数定义中如果参数列表中若干个相邻参数的类型相同,则可以在参数列表中省略前面的变量类型声明。

func Add(a, b int) int { //这里a和b都是int类型 // 函数体 }

 

package main import "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 }

结果

最大值是 : 200

 

多值返回

Go语言的函数可以返回不止一个结果,即支持“多值返回”。

Go语言函数多值返回一般用于处理错误。比如在IO操作时候,不一定每次都能成功:可能文件不存在或者磁盘损坏无法读取文件。所以一般在函数调用发生错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回。

 

func SumProductDiff(i, j int) (int, int, int) { return i+j, i*j, i-j }

package main import "fmt" func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("Mahesh", "Kumar") fmt.Println(a, b) }

func SumAndProduct(A, B int) (add int, Multiplied int) { add = A+B Multiplied = A*B return }

 

不定参数

如果你对Python比较熟悉,你会知道Python函数参数个数可以是不确定的。和Python类似,Go语言函数也支持可变的参数个数。

在参数列表的最后类型名称前面使用省略号“…”来表示声明一个变长函数,调用该函数的时候可以传递该类型任意数目的参数。

func 函数名(args ...Type) (返回值列表) { // 函数体 }

 

代码示例

我们在这里写三个函数,依次展示出函数最常态模样,多值返回和不定参数。

package main import( "fmt" ) func Add(i int, j int) (int) { //常规函数 return i+j } func Add_Multi_Sub(i, j int) (int, int, int) { //多值返回 return i+j, i*j, i-j } func sum(nums ...int) { //变参函数 total := 0 for _, num := range nums { total += num } fmt.Println(total) } func main(){ a, b := 2,3 arr := []int{1, 2, 3} var c int = Add(a,b) d,e,f := Add_Multi_Sub(a,b) fmt.Println(c,d,e,f) sum(arr...) //注意传参形式 }

 

值传递

package main import ( "fmt" "math" ) func main(){ /* 声明函数变量 */ getSquareRoot := func(x float64) float64 { return math.Sqrt(x) } /* 使用函数 */ fmt.Println(getSquareRoot(9)) }

 

引用传递

这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内 存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以我们需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子

package main import "fmt" //简单的一个函数,实现了参数+1的操作 func add1(a *int) int { // 请注意, *a = *a+1 // 修改了a的值 return *a // 返回新值 } f func main() { x := 3 fmt.Println("x = ", x) // 应该输出 "x = 3" x1 := add1(&x) // 调用 add1(&x) 传x的地址 fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" fmt.Println("x = ", x) // 应该输出 "x = 4" }

  • 传指针使得多个函数能操作同一个对象。
  • 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
  • Go语言中slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递 指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)

 

Go语言支持匿名函数,即函数可以像普通变量一样被传递或使用。

匿名函数的英文表示是:Anonymous functions Functions with name are named functions!

说白了,匿名函数是指不需要定义函数名的一种函数实现方式

 

使用方法如下:

package main import ( "fmt" ) func main() { var v func(a int) int v = func(a int) int { return a * a } fmt.Println(v(6)) //两种写法 v1 := func(i int) int { return i * i } fmt.Println(v1(7)) }

 

匿名函数作为返回值

func getPrintMessage() func(string) { // returns an anonymous function return func(message string) { fmt.Println(message) } }

 

匿名函数付给变量

f := func() int { ... }

 

返回多个匿名函数

func calc(x, y int) (func(int), func()) { f1 := func(z int) int { return (x + y) * z / 2 } f2 := func() int { return 2 * (x + y) } return f1, f2 }

 

匿名函数,可作为闭包, 是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

package main import "fmt" func getSequence() func() int { i:=0 return func() int { i+=1 return i } } func main(){ /* nextNumber 为一个函数,函数 i 为 0 */ nextNumber := getSequence() /* 调用 nextNumber 函数,i 变量自增 1 并返回 */ fmt.Println(nextNumber()) fmt.Println(nextNumber()) fmt.Println(nextNumber()) /* 创建新的函数 nextNumber1,并查看结果 */ nextNumber1 := getSequence() fmt.Println(nextNumber1()) fmt.Println(nextNumber1()) }

 

匿名函数 2

func(i, j int) (m, n int) { // x y 为函数返回值

return j, i

}(1, 9) // 直接创建匿名函数并执行

}

 

何为闭包

关于闭包的概念,有些抽象。

WIKI:

In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions.

A closure is a function value that references variables from outside its body.

个人理解:

闭包就是能够读取其他函数内部变量的函数。

只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

 

返回闭包

package main import "fmt" func outer(name string) func() { // variable text := "Modified " + name // closure. function has access to text even after exiting this block foo := func() { fmt.Println(text) } // return the closure return foo } func main() { // foo is a closure foo := outer("hello") // calling a closure foo() }

 

函数做为值

在Go中函数也是一种变量,我们可以通过type来定义它

**同种方法:**参数类型、个数、顺序相同,返回值相同

一般步骤:

定义一个函数类型

实现定义的函数类型

作为参数调用

 

package main import "fmt" type testInt func(int) bool // 声明了一个函数类型 func isOdd(integer int) bool { if integer%2 == 0 { return false } return true } func isEven(integer int) bool { if integer%2 == 0 { return true } return false } func filter(slice []int, f testInt) []int { var result []int for _, value := range slice { if f(value) { result = append(result, value) } } return result } func main(){ slice := []int {1, 2, 3, 4, 5, 7} fmt.Println("slice = ", slice) odd := filter(slice, isOdd) // 函数当做值来传递了 fmt.Println("Odd elements of slice are: ", odd) even := filter(slice, isEven) // 函数当做值来传递了 fmt.Println("Even elements of slice are: ", even) }

type testInt func(int) bool就是将该种函数类型赋值给testInt

 

Defer

即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题

  • 如果有很多调用defer,那么defer是采用后进先出模式
  • 在离开所在的方法时,执行(报错的时候也会执行)

func ReadWrite() bool { file.Open("file") defer file.Close() if failureX { return false } f failureY { return false } return true }

最后才执行file.Close()

示例

package main import "fmt" func main() { a := 1 b := 2 defer fmt.Println(b) fmt.Println(a) }

结果

1 2

 

执行 recover

被 defer 的函数在 return 之后执行,这个时机点正好可以捕获函数抛出的 panic,因而 defer 的另一个重要用途就是执行 recover。

recover 只有在 defer 中使用才更有意义,如果在其他地方使用,由于程序已经调用结束而提前返回而无法有效捕捉错误。

package main import ( "fmt" ) func main() { defer func() { if ok := recover(); ok != nil { fmt.Println("recover") } }() panic("error") }

 

方法

在 Go 语言中有一个概念和函数极其相似,叫做方法 。Go 语言的方法其实是作用在接收者(receiver)上的一个函数,接收者是某种非内置类型的变量。因此方法是一种特殊类型的函数。

接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型。

方法的声明和普通函数的声明类似,只是在函数名称前面多了一个参数,这个参数把这个方法绑定到这个参数对应的类型上。

如果method的名字一模一样,但是如果接收者不一样,那么method就不一样。

func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ }

 

package main import ( "fmt" ) /* 定义函数 */ type Circle struct { radius float64 } func main() { var c1 Circle c1.radius = 10.00 fmt.Println("Area of Circle(c1) = ", c1.getArea()) } //该 method 属于 Circle 类型对象中的方法 func (c Circle) getArea() float64 { //c.radius 即为 Circle 类型对象中的属性 return 3.14 * c.radius * c.radius }

 

和函数关系

方法是特殊的函数,定义在某一特定的类型上,通过类型的实例来进行调用,这个实例被叫接收者(receiver)。

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

接收者必须有一个显式的名字,这个名字必须在方法中被使用。

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。

方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。

注意: Go语言不允许为简单的内置类型添加方法,所以下面定义的方法是非法的。

package main import( "fmt" ) func Add(a ,b int){ //函数合法 fmt.Println(a+b) } func (a int) Add (b int){ //方法非法!不能是内置数据类型 fmt.Println(a+b) }

 

这个时候我们需要用Go语言的type,来临时定义一个和int具有同样功能的类型。这个类型不能看成是int类型的别名,它们属于不同的类型,不能直接相互赋值。

修改后合法的方法定义如下:

package main import( "fmt" ) type myInt int func Add(a ,b int){ //函数 fmt.Println(a+b) } func (a myInt) Add (b myInt){ //方法 fmt.Println(a+b) } func main() { a, b := 3,4 var aa,bb myInt = 3,4 Add(a,b) aa.Add(bb) }

 

上面的表达式aa.Add称作选择子(selector),它为接收者aa选择合适的Add方法。

 

指针作为接收者

若不是以指针作为接收者,实际只是获取了一个copy,而不能真正改变接收者的中的数据

func (b *Box) SetColor(c Color) { b.color = c }

 

package main import ( "fmt" ) type Rectangle struct { width, height int } func (r *Rectangle) setVal() { r.height = 20 } func main() { p := Rectangle{1, 2} s := p p.setVal() fmt.Println(p.height, s.height) }

结果

20 2

如果没有那个*,则值就是2 2

 

method继承

method是可以继承的,如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method

package main import "fmt" type Human struct { name string age int phone string } type Student struct { Human //匿名字段 school string } type Employee struct { Human //匿名字段 company string } func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } func main() { mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} mark.SayHi() sam.SayHi() }

 

method重写

package main import "fmt" type Human struct { name string age int phone string } type Student struct { Human //匿名字段 school string } type Employee struct { Human //匿名字段 company string } //Human定义method func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } //Employee的method重写Human的method func (e *Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //Yes you can split into 2 lines here. } func main() { mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} mark.SayHi() sam.SayHi() }

 

接口

Go 语言不是一种“传统” 的面向对象编程语言, 所以 Go 语言并没有类和继承的概念。

但是 Go 语言里有非常灵活的接口概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来说明对象的行为。在Go语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是Go语言中所有数据结构的核心。

接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为。

Go语言中的接口是一些方法的集合(method set),它指定了对象的行为:如果它(任何数据类型)可以做这些事情,那么它就可以在这里使用。看一种类型是不是“实现”了一个接口,就得看这种类型是不是实现了接口中定义的所有方法。

 

接口定义

接口定义了一组方法集合,但是这些方法不包含具体的实现代码。还有需要强调一点:接口定义中不能包含变量。

一般通过如下格式定义接口:

/* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] ... method_namen [return_type] }

 

在定义了一个接口之后,一般使用一个自定义结构体(struct)去实现接口中的方法。

/* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] ... method_namen [return_type] } /* 定义结构体 */ type struct_name struct { /* variables */ } /* 实现接口方法 */ func (struct_name_variable struct_name) method_name1() [return_type] { /* 方法实现 */ } ... func (struct_name_variable struct_name) method_namen() [return_type] { /* 方法实现*/ }

 

其实对于某个接口的同一个方法,不同的结构体(struct)可以有不同的实现,例如:

package main import ( "fmt" ) type Phone interface { call() } type NokiaPhone struct {} type IPhone struct {} func (nokiaPhone NokiaPhone) call() { fmt.Println("I am Nokia, I can call you!") } func (iPhone IPhone) call() { fmt.Println("I am iPhone, I can call you!") } func main() { var phone Phone phone = new(NokiaPhone) phone.call() phone = new(IPhone) phone.call() }

 

接口的值

不像大多数面向对象编程语言,在 Go 语言中接口可以有值。假设有个接口定义如下:

type Namer interface { Method1(param_list) return_type Method2(param_list) return_type ... }

 

那么定义var ai Namer中,ai是有值的,只不过这个时候,ai是接口的“零值”状态,值是 nil。

事实上 ,Go 语言中接口不仅有值,还能进行接口赋值,接口赋值分为以下两种情况:

1. 将对象实例赋值给接口。

2. 将一个接口赋值给另一个接口。

其中:

将对象实例赋值给接口要求对象实现了接口的所有方法;

接口之间的赋值要求接口A中定义的所有方法,都在接口B中有定义,那么B接口的实例可以赋值给A的对象。反之不一定成立,除非A和B定义的方法完全一样(顺序不要求),这时A和B等价,可以相互赋值。

示例代码如下:

package main import "fmt" type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { sq1 := new(Square) sq1.side = 5 // 赋值方法1: //var areaIntf Shaper // areaIntf = sq1 // 更短的赋值方法2: // areaIntf := Shaper(sq1) // 最简洁的赋值方法3: areaIntf := sq1 fmt.Printf("The square has area: %f\n", areaIntf.Area()) }

 

【注意】接口对象不能调用接口实现对象的属性

 

指针

一个指针可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上*号(前缀)来获取指针所指向的内容,这里的*号是一个类型更改器。使用一个指针引用一个值被称为间接引用。

 

Go语言指针

在Go语言中,直接砍掉了 C 语言指针最复杂的指针运算部分,只留下了获取指针(&运算符)和获取对象(*运算符)的运算,用法和C语言很类似。但不同的是,Go语言中没有->操作符来调用指针所属的成员,而与一般对象一样,都是使用.来调用。

Go 语言中一个指针被定义后没有分配到任何变量时,它的值为nil。

Go 语言自带指针隐式解引用 :对于一些复杂类型的指针, 如果要访问成员变量时候需要写成类似*p.field的形式时,只需要p.field即可访问相应的成员。

 

new函数

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:

var t *T = new(T)

这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。

注意:

用new(structName):这个方法得到的是*structName类型,即类的指针类型;

用structName{init para}:这个方法得到的是structName类型,即类的实例类型,不是指针。

示例代码如下:

package main import ( "fmt" ) type Student struct { name string age int weight float32 score []int } func main(){ pp := new(Student) //使用 new 关键字创建一个指针 *pp = Student{"qishuangming", 23, 65.0, []int{2, 3, 6}} fmt.Printf("stu pp have %d subjects\n", len((*pp).score)) fmt.Printf("stu pp have %d subjects\n", len(pp.score)) //Go语言自带隐式解引用 }

结果: stu pp have 3 subjects

stu pp have 3 subjects

 

结构体和指针的综合应用

初始化一个结构体实例,除了上面讲的值类型初始化和结构体指针初始化,还有一种更简短和惯用的方式叫字面量初始化(结构体字面量:struct-literal):

type struct1 struct { i1 int f1 float32 str string } ms := &struct1{10, 15.5, "Chris"} // 此时ms的类型是 *struct1

 

混合字面量语法(composite literal syntax)&struct1{a, b, c} 是一种简写,底层仍然会调用new (),这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。所以归根结底,表达式 new(Type) 和 &Type{}是等价的。

 

下面的例子显示了一个结构体Person,一个方法,方法有一个类型为 *Person的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:

package main import ( "fmt" "strings" ) type Person struct { firstName string lastName string } func upPerson(p *Person) { p.firstName = strings.ToUpper(p.firstName) p.lastName = strings.ToUpper(p.lastName) } func main() { // 1-struct as a value type: var pers1 Person pers1.firstName = "Chris" pers1.lastName = "Woodward" upPerson(&pers1) fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName) // 2—struct as a pointer: pers2 := new(Person) pers2.firstName = "Chris" pers2.lastName = "Woodward" (*pers2).lastName = "Woodward" // 这是合法的 upPerson(pers2) fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName) // 3—struct as a literal: pers3 := &Person{"Chris","Woodward"} upPerson(pers3) fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName) } //output /* The name of the person is CHRIS WOODWARD The name of the person is CHRIS WOODWARD The name of the person is CHRIS WOODWARD */

 

Go 空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。 nil 指针也称为空指针。 nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。 一个指针变量通常缩写为 ptr。

空指针判断:

if(ptr != nil) /* ptr 不是空指针 */ if(ptr == nil) /* ptr 是空指针 */

 

指针数组

package main import "fmt" const MAX int = 3 func main() { a := []int{10,100,200} var i int var ptr [MAX]*int; for i = 0; i < MAX; i++ { ptr[i] = &a[i] /* 整数地址赋值给指针数组 */ } for i = 0; i < MAX; i++ { fmt.Printf("a[%d] = %d\n", i,*ptr[i] ) } }

 

指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。

var ptr **int;

package main import "fmt" func main() { var a int var ptr *int var pptr **int a = 3000 /* 指针 ptr 地址 */ ptr = &a /* 指向指针 ptr 地址 */ pptr = &ptr /* 获取 pptr 的值 */ fmt.Printf("变量 a = %d\n", a ) fmt.Printf("指针变量 *ptr = %d\n", *ptr ) fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr) }

 

切片(Slice)

Go 语言切片是对数组的抽象。 Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大

切片与数组相比,不需要设定长度,在[]中不用设定值,相对来说比较自由

从概念上面来说slice像一个结构体,这个结构体包含了三个元素:

1. 指针,指向数组中slice指定的开始位置

2. 长度,即slice的长度

3. 最大长度,也就是slice开始位置到数组的最后位置的长度

 

定义切片(注意同数组定义的区别)

var identifier []type

切片不需要说明长度。 或使用make()函数来创建切片:

var slice1 []type = make([]type, len) 也可以简写为 slice1 := make([]type, len)

make([]T, length, capacity)

 

初始化

s[0] = 1 s[1] = 2 s[2] = 3

s :=[] int {1,2,3 }

s := arr[startIndex:endIndex]

将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片(前闭后开),长度为endIndex-startIndex

s := arr[startIndex:]

缺省endIndex时将表示一直到arr的最后一个元素

s := arr[:endIndex]

缺省startIndex时将表示从arr的第一个元素开始

len() 和 cap() 函数 切片是可索引的,并且可以由 len() 方法获取长度 切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少

package main import "fmt"

func main() { var numbers = make([]int,3,5) printSlice(numbers) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

结果

len=3 cap=5 slice=[0 0 0]

 

空切片

一个切片在未初始化之前默认为 nil,长度为 0

package main import "fmt" func main() { var numbers []int printSlice(numbers) if(numbers == nil){ fmt.Printf("切片是空的") } } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

结果

len=0 cap=0 slice=[] 切片是空的

package main import "fmt" func main() { /* 创建切片 */ numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers) /* 打印原始切片 */ fmt.Println("numbers ==", numbers) /* 打印子切片从索引1(包含) 到索引4(不包含)*/ fmt.Println("numbers[1:4] ==", numbers[1:4]) /* 默认下限为 0*/ fmt.Println("numbers[:3] ==", numbers[:3]) /* 默认上限为 len(s)*/ fmt.Println("numbers[4:] ==", numbers[4:]) numbers1 := make([]int,0,5) printSlice(numbers1) /* 打印子切片从索引 0(包含) 到索引 2(不包含) */ number2 := numbers[:2] printSlice(number2) /* 打印子切片从索引 2(包含) 到索引 5(不包含) */ number3 := numbers[2:5] printSlice(number3) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

结果

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8] numbers == [0 1 2 3 4 5 6 7 8] numbers[1:4] == [1 2 3] numbers[:3] == [0 1 2] numbers[4:] == [4 5 6 7 8] len=0 cap=5 slice=[] len=2 cap=9 slice=[0 1] len=3 cap=7 slice=[2 3 4]

 

append() 和 copy() 函数

append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数

append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩 余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原 数组的内容将保持不变;其它引用此数组的slice则不受影响

下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法

package main import "fmt" func main() { var numbers []int printSlice(numbers) /* 允许追加空切片 */ numbers = append(numbers, 0) printSlice(numbers) /* 向切片添加一个元素 */ numbers = append(numbers, 1) printSlice(numbers) /* 同时添加多个元素 */ numbers = append(numbers, 2,3,4) printSlice(numbers) /* 创建切片 numbers1 是之前切片的两倍容量*/ numbers1 := make([]int, len(numbers), (cap(numbers))*2) /* 拷贝 numbers 的内容到 numbers1 */ copy(numbers1,numbers) printSlice(numbers1) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

结果

len=0 cap=0 slice=[] len=1 cap=2 slice=[0] len=2 cap=2 slice=[0 1] len=5 cap=8 slice=[0 1 2 3 4] len=5 cap=12 slice=[0 1 2 3 4]

numbers1与numbers两者不存在联系,numbers发生变化时,numbers1是不会随着变化的。也就是说copy方法是不会建立两个切片的联系的

 

slice的合并

append的第一个参数是slice,第二个参数是元素。结合下Go语言函数的不定参数,我们可以用append优雅地实现两个slice的拼接。例如,下面的函数能合并两个slice的全部元素到一个新的slice,并返回新slice的长度len和容量cap:

func slice_add(slice01[]int,slice02[]int)(int,int){ new_slice := append(slice01,slice02...) return len(new_slice),cap(new_slice) }

 

slice作为函数参数

注意和数组作为函数参数的区别:

package main import ( "fmt" ) func main() { a := []int{1, 2, 3, 4, 5} var b = a[0:3] var c = [...]int{3, 6, 9, 2, 6, 4} d := c[0:2] sliceInfo(b) fmt.Printf("sum of b is %d\n", sum(b)) fmt.Printf("sum of d is %d\n", sum(d)) } func sum(a []int) int { s := 0 for i := 0; i < len(a); i++ { s += a[i] } return s } func sliceInfo(x []int) { fmt.Printf("len is %d ,cap is %d, slice is %v\n", len(x), cap(x), x) }

 

slice的不可比性

Go 语言中slice 和map ,func一样,不支持 ==操作符,就是不能直接比较,唯一合法的就是和nil作比较。开发中经常会遇到需要比较两个slice包含的元素是否完全相等的情况,这个时候只能遍历两个slice中的所有元素 ,看它们是否完全相等。

 

删除切片的方法:

for i, v := range vect {

if v == -1 { //当V=-1时,假定是不需要的数据

vect = append(vect[:i], vect[i+1:]...)

}

}

 

作为函数参数的切片(https://studygolang.com/articles/9876

直接改变切片

回到最开始的问题,当函数的参数是切片的时候,到底是传值还是传引用?从changeSlice函数中打出的参数s的地址,可以看出肯定不是传引用,毕竟引用都是一个地址才对。然而changeSlice函数内改变了s的值,也改变了原始变量slice的值,这个看起来像引用的现象,实际上正是我们前面讨论的切片共享底层数组的实现。

即切片传递的时候,传的是数组的值,等效于从原始切片中再切了一次。原始切片slice和参数s切片的底层数组是一样的。因此修改函数内的切片,也就修改了数组。

 

 

集合(Map)

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值 Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的,也是引用类型。这是一种快速寻找值的理想结构:给定key,对应的value可以迅速定位。map 这种数据结构在其他编程语言中也称为字典(Python)、hash 和HashTable 等。

使用map过程中需要注意的几点: - map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必 须通过key获取 - map的长度是不固定的,也就是和slice一样,也是一种引用类型 - 内置的len函数同样适用于 map,返回map拥有的key的数量 - map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为 one的字典值改为11

 

map声明和初始化

map 是引用类型,可以使用如下声明:

make(map[KeyType]ValueType, initialCapacity) make(map[KeyType]ValueType) map[KeyType]ValueType{} map[KeyType]ValueType{key1 : value1, key2 : value2, ... , keyN : valueN}

 

未初始化的 map 的值是 nil。

用4种方式分别创建数组,其中第一种和第二种的区别在于,有没有指定初始容量,不过使用的时候则无需在意这些,因为map的本质决定了,一旦容量不够,它会自动扩容。示例代码如下:

func test1() { map1 := make(map[string]string, 5) map2 := make(map[string]string) map3 := map[string]string{} map4 := map[string]string{"a": "1", "b": "2", "c": "3"} fmt.Println(map1, map2, map3, map4) }

 

注意:必须要先初始化才能给map赋值设置元素,不然会引起 panic: assign to entry in nil map。

示例代码段如下:

package main import( "fmt" ) func main(){ ages01 := map[string]int{ "alice":31, "bob":13, } ages02 := make(map[string]int) ages02["chris"] =20 ages02["paul"] = 30 //age01和age02两种初始化的方式等价 m1 := make(map[string]int) m2 := map[string]int{} //m1和m2创建方式等价,都是创建了一个空的的map,这个时候m1和m2没有任何元素 for name,age := range ages01{ fmt.Printf("%s\t%d\n",name,age) } for name,age := range ages02{ fmt.Printf("%s\t%d\n",name,age) } var null_map map[string]int //声明但未初始化map,此时是map的零值状态(只有一个nil元素) empty_map := map[string]int{} //创建了初始化了一个空的的map,这个时候empty_map没有任何元素 fmt.Println(m1 != nil && m2 != nil) //true fmt.Println(len(null_map)==0) fmt.Println(null_map ==nil) //true,此时是map的零值状态(nil) fmt.Println(len(empty_map)==0) fmt.Println(empty_map ==nil) //false,空的的map不等价于nil(map的零值状态) empty_map["test"] = 12 //执行正常,空的的map可以赋值设置元素 null_map["test"] = 12 //panic: assignment to entry in nil map,无法给未初始化的map赋值设置元素 }

 

map元素遍历

range for可用于遍历map 中所有的元素,不过需要注意因为 map本身是无序的,因此对于程序的每次执行,不能保证使用 range for 遍历 map的顺序总是一致的。例如:

package main import ( "fmt" ) func main() { personSalary := map[string]int{ "steve": 12000, "jamie": 15000, } personSalary["mike"] = 9000 fmt.Println("All items of a map") for key, value := range personSalary { fmt.Printf("personSalary[%s] = %d\n", key, value) } }

 

map元素增删改查

首先这里map元素的增加和修改元素的语法一致,只需要map[K]=V即可。例如:

package main import ( "fmt" ) func main() { personSalary := make(map[string]int) personSalary["steve"] = 12000 //增加元素 personSalary["jamie"] = 15000 //增加元素 personSalary["mike"] = 9000 //增加元素 fmt.Println("map before change", personSalary) personSalary["mike"] = 10000 //修改元素 fmt.Println("map after change", personSalary) } //output /* map before change map[steve:12000 jamie:15000 mike:9000] map after change map[steve:12000 jamie:15000 mike:10000] */

 

删除元素需要使用内置函数delete

该函数根据键来删除一个元素。需要强调delete函数没有返回值,例如:

package main import ( "fmt" ) func main() { personSalary := map[string]int{ "steve": 12000, "jamie": 15000, } personSalary["mike"] = 9000 fmt.Println("map before deletion", personSalary) delete(personSalary, "steve") fmt.Println("map after deletion", personSalary) } //output /* map before deletion map[steve:12000 jamie:15000 mike:9000] map after deletion map[mike:9000 jamie:15000] */

 

ok-idiom

使用ok-idiom获取值,可知道key/value是否存在

package main import ( "fmt" ) func main() { m := make(map[string]int) m["a"] = 1 x, ok := m["b"] fmt.Println(x, ok) x, ok = m["a"] fmt.Println(x, ok) }

结果

0 false 1 true

 

查找 map 中某个元素需要用下面的代码段:

if _, ok := map[key]; ok { //如果存在则执行 }

 

这里需要强调下,根据键值索引某个元素时,也会返回两个值:索引到的值和本次索引是否成功(这里可能会因为索数值越界或者索引键值有误而导致索引失败)。

示例代码如下:

package main import( "fmt" ) func main(){ ages01 := map[string]int{ "alice":31, "bob":13, } age,ok := ages01["bo"] //age才是根据键值索引到的值 if !ok{ fmt.Printf("索引失败,bo不是map的键值,此时age=%d",age) //索引失败会返回value的零值,这里是int类型,所以是0 } else{ fmt.Printf("索引成功,age=%d",age) } }

 

make、new操作

make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配 内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要: new返回指针

内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初 始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。

make返回初始化后的(非零)值。

 

map的不可比性

Go 语言中map和slice,func一样,不支持 == 操作符,就是不能直接比较。唯一合法的就是和nil作比较,判断该map是不是零值状态。

如果想自定义一个函数,来比较两个map是否相等,就可以遍历比较它们的键和值是否完全相等,代码如下:

func map_equal(x,y map[string] string) bool{ if(len(x))!= len(y){ return false } for k,xv :=range x{ if yv,ok:=y[k];!ok||yv!=xv{ return false } } return true }

 

反射

所谓反射就是动态运行时的状态。我们一般用到的包是reflect包

使用reflect一般分成三步:

首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)

t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

 

获取反射值能返回相应的类型和数值

var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())

如果是struct的话,可以使用Elem()

tag := t.Elem().Field(0).Tag //获取定义在struct里面的Tag属性 name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

修改

var x float64 = 3.4 p := reflect.ValueOf(&x) v := p.Elem()//必须的步骤 v.SetFloat(7.1)

 

性能代价

Go语言反射在带来“方便”的同时,会造成问题:它会带来很大的性能损失。直接赋值和反射赋值在实际使用中性能差异挺大,所以如果对性能要求较高,那么请谨慎使用反射。

 

Go语言类型

Go语言是静态类型的编程语言,所有数据的类型在编译期确定了。

而且 Go 语言中即使是底层存的是一个类型,声明的类型不一样,也要强制转换才能互用。

例如:

type MyInt int var i int var j MyInt

 

这里的 i 和 j 类型不一致 ,如果需要进行比较或者加减运算 ,需要强制转换类型。

注意:在 Go 语言里面没有隐式转换,遇到不同类型想互用,只能进行强制类型转换。

空接口

Go语言空interface(interface{})不包含任何的method,因此所有的类型都实现了空interface,空interface在我们需要存储任意类型的数值的时候相当有用。我们通过一个例子来看一下空接口的强大:

package main import ( "fmt" ) func main() { slice := make([]interface{}, 10) map1 := make(map[string]string) map2 := make(map[string]int) map2["TaskID"] = 1 map1["Command"] = "ping" map3 := make(map[string]map[string]string) map3["mapvalue"] = map1 slice[0] = map2 slice[1] = map1 slice[3] = map3 fmt.Println(slice[0]) fmt.Println(slice[1]) fmt.Println(slice[3]) }

 

这段代码声明了一个空接口的slice,这意味着它的值可以是任意类型,然后我们声明了两个map,一个是map[string]string,一个是map[string]int,然后在声明一个map的map类型,将这三个类型赋值给slice,使得slice可以存贮各种不同类型的数据,想想看,一个可变数组中,存储了一个key为string类型,value为int类型的map,又存储了一个key为string类型,value为string类型的map,还存储了一个map的map。

go的反射机制是要通过接口来进行的,而类似于Java的Object的空接口可以和任何类型进行交互,因此对基本数据类型等的反射也直接利用了这一特点。

 

自定义类型

使用关键字type就可以开始自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等

type name int8

多个type还可以合成一个组

package main import ( "fmt" ) type ( user struct { name string age uint8 } event func(string) bool ) func main() { u := user{"Jason", 20} fmt.Println(u) var f event = func(s string) bool { fmt.Println(s) return s != "" } f("abc") }

结果

{Jason 20} abc

注意

package main import ( "fmt" ) type data int func main() { var d data = 10 var x int = d fmt.Println(x) fmt.Println(d == x) }

 

d虽然底层的数据类型是int,但其与int是两个不同的概念,不能作为相同的数据类型看待

 

未命名类型

与有明确标识符的bool、int、string等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等属性有关,故称作未命名类型。当然,可用type为其提供具体的名称,将其改变为命名类型

  • 具有相同基类型的指针
  • 具有相同元素类型和长度的数组
  • 具有相同元素类型的切片
  • 具有相同键值类型的字典
  • 具有相同数据类型及操作方向的通道
  • 具有相同字段序列(字段名、字段类型、标签、以及字段顺序)的结构体
  • 具有相同签名(参数的返回值列表,不包括参数名)的函数
  • 具有相同方法集(方法名、方法签名、不包括顺序)的接口

未命名类型转换规则

  • 所属类型相同
  • 基础类型相同,且其中一个是未命名类型
  • 数据类型相同,将双向通道给单向通道,且其中一个为未命名类型
  • 将默认值nil赋值给切片、字典、通道、指针、函数或接口
  • 对象实现了目标接口

package main import ( "fmt" ) type data [2]int func main() { var d data = [2]int{1, 2} fmt.Println(d) a := make(chan int, 2) var b chan<- int = a b <- 2 }

结果

[1 2]

 

go语言变量声明后的默认值

在go语言中,任何类型在声明后没有赋值的情况下,都对应一个零值。

  • 整形如int8、byte、int16、uint、uintprt等,默认值为0。
  • 浮点类型如float32、float64,默认值为0。
  • 布尔类型bool的默认值为false。
  • 复数类型如complex64、complex128,默认值为0+0i。
  • 字符串string的默认值为”“。
  • 错误类型error的默认值为nil。
  • 对于一些复合类型,如指针、切片、字典、通道、接口,默认值为nil。而数组的默认值要根据其数据类型来确定。例如:var a [4]int,其默认值为[0 0 0 0]。

 

进程,线程,并行和并发

一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有’正式’的程序都是多线程的,以便让用户或计算机不必等待,或者能够同时服务多个请求(如 Web 服务器),或增加性能和吞吐量(例如,通过对不同的数据集并行执行代码)。

一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有在同一个程序在某一个时间点在多个些处理内核或处理器上同时执行的任务才是真正的并行。

并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。

公认的使用多线程的应用最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作竞态)。

在Go中,应用程序并发处理的部分被称作 goroutines(go协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在它们之上的;协程调度器在 Go 运行时很好的完成了这个工作。

 

Goroutine简介

Go语言中有个概念叫做goroutine, 这类似我们熟知的线程,但是更加轻量级。

我们先来看一个没有并发的例子,串行地去执行两次loop函数:

package main import "fmt" func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } } func main() { loop() loop() }

 

这里的输出是显而易见的,是0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。

现在我们把一个loop放在一个goroutine里跑,我们可以使用关键字go来定义并启动一个goroutine,main()函数变为:

func main() { go loop() loop() }

 

就加了一个 go 关键字 ,输入就变成了:

0 1 2 3 4 5 6 7 8 9

可是为什么只输出了一趟呢?明明我们主线跑了一趟,也开了一个goroutine来跑一趟啊。

原来,在goroutine还没来得及跑loop的时候,主函数已经退出了。

如何让goroutine告诉主线程我执行完毕了?使用一个信道来告诉主线程即可。

无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。如果不用信道来阻塞主线的话,主线程就会过早跑完,loop线程都没有机会执行。

本关卡需要强调的是,无缓冲的信道永远不会存储数据,只负责数据的流通。体现在:

从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞

数据流入无缓冲信道, 如果没有其他goroutine来拿走这个数据,那么当前线阻塞

现在这里给出代码,具体Channel的原理和使用在本实训下一关卡Go语言Channel中详细讲解。

var complete chan int = make(chan int) func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } complete <- 0 // 执行完毕了,发个消息 } func main() { go loop() <- complete // 直到线程跑完, 取到消息. main在此阻塞住 }

 

 

Actor模型和CSP模型的区别

   Akka/Erlang的actor模型与Go语言的协程Goroutine与通道Channel代表的CSP(Communicating Sequential Processes)模型有什么 区别呢?

  首先这两者都是并发模型的解决方案,我们看看Actor和Channel这两个方案的不同:

Actor模型

  在Actor模型中,主角是Actor,类似一种worker,Actor彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的:

  Actor模型描述了一组为了避免并发编程的常见问题的公理:

  1.所有Actor状态是Actor本地的,外部无法访问。

  2.Actor必须只有通过消息传递进行通信。  

  3.一个Actor可以响应消息:推出新Actor,改变其内部状态,或将消息发送到一个或多个其他参与者。

  4.Actor可能会堵塞自己,但Actor不应该堵塞它运行的线程。

  更多可见Actor模型专题

Actor模型

在使用Java进行并发编程时需要特别的关注锁和内存原子性等一系列线程问题,而Actor模型内部的状态由它自己维护即它内部数据只能由它自己修改(通过消息传递来进行状态修改),所以使用Actors模型进行并发编程可以很好地避免这些问题,Actor由状态(state)、行为(Behavior)和邮箱(mailBox)三部分组成

状态(state):Actor中的状态指的是Actor对象的变量信息,状态由Actor自己管理,避免了并发环境下的锁和内存原子性等问题

行为(Behavior):行为指定的是Actor中计算逻辑,通过Actor接收到消息来改变Actor的状态

邮箱(mailBox):邮箱是Actor和Actor之间的通信桥梁,邮箱内部通过FIFO消息队列来存储发送方Actor消息,接受方Actor从邮箱队列中获取消息

Actor的基础就是消息传递

使用Actor模型的好处:

事件模型驱动--Actor之间的通信是异步的,即使Actor在发送消息后也无需阻塞或者等待就能够处理其他事情

强隔离性--Actor中的方法不能由外部直接调用,所有的一切都通过消息传递进行的,从而避免了Actor之间的数据共享,想要

观察到另一个Actor的状态变化只能通过消息传递进行询问

位置透明--无论Actor地址是在本地还是在远程机上对于代码来说都是一样的

轻量性--Actor是非常轻量的计算单机,单个Actor仅占400多字节,只需少量内存就能达到高并发

 

Channel模型

  Channel模型中,worker之间不直接彼此联系,而是通过不同channel进行消息发布和侦听。消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。

  Go语言的CSP模型是由协程Goroutine与通道Channel实现:

  • Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。
  • 通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。
 

Actor模型和CSP区别

  Actor模型和CSP区别图如下:

  Actor之间直接通讯,而CSP是通过Channel通讯,在耦合度上两者是有区别的,后者更加松耦合。

  同时,它们都是描述独立的进程通过消息传递进行通信。主要的区别在于:在CSP消息交换是同步的(即两个进程的执行"接触点"的,在此他们交换消息),而Actor模型是完全解耦的,可以在任意的时间将消息发送给任何未经证实的接受者。由于Actor享有更大的相互独立,因为他可以根据自己的状态选择处理哪个传入消息。自主性更大些。

  在Go语言中为了不堵塞进程,程序员必须检查不同的传入消息,以便预见确保正确的顺序。CSP好处是Channel不需要缓冲消息,而Actor理论上需要一个无限大小的邮箱作为消息缓冲。

 

Golang并发编程之Channel

 

Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。

它的操作符是箭头 <- 。

ch <- v // 发送值v到Channel ch中

v := <- ch // 从Channel ch中接收数据,并将数据赋值给v

(箭头的指向就是数据的流向)

 

就像 map 和 slice 数据类型一样, channel必须先创建再使用:

ch := make(chan int)

 

Channel定义

Go 语言中Channel类型的定义格式如下:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

 

它包括三种类型的定义。可选的<-代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。

chan T // 可以接收和发送类型为 T 的数据 chan <- float64 // 只可以用来发送 float64 类型的数据(箭头指向就是数据流向,把数据发送到通道) <-chan int // 只可以用来接收 int 类型的数据

 

注意:<- 总是优先和最左边的类型结合(The <- operator associates with the leftmost chan possible)。

默认情况下,channel发送方和接收方会一直阻塞直到对方准备好发送或者接收,这就使得Go语言无需加锁或者其他条件,天然支持了并发。

c := make(chan bool) //创建一个无缓冲的bool型Channel c <- x //向一个Channel发送一个值 <- c //从一个Channel中接收一个值 x = <- c //从Channel c接收一个值并将其存储到x中 x, ok = <- c //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

 

使用 make 初始化Channel,还可以设置容量:

make(chan int, 100) #//创建一个有缓冲的int型Channel

 

Channel缓冲介绍

容量(capacity)代表Channel容纳的最多的元素的数量,代表Channel的缓冲的大小。 如果没有设置容量,或者容量设置为0, 说明Channel没有缓冲。

在实际开发中,你可以在多个goroutine从/往一个channel 中 receive/send 数据, 不必考虑额外的同步措施。

Channel可以作为一个先入先出(FIFO)的队列,接收的数据和发送的数据的顺序是一致的。

不带缓冲的Channel兼具通信和同步两种特性,在并发开发中颇受青睐。例如:

// _Channels_ are the pipes that connect concurrent // goroutines. You can send values into channels from one // goroutine and receive those values into another // goroutine. package main import "fmt" func main() { // Create a new channel with `make(chan val-type)`. // Channels are typed by the values they convey. messages := make(chan string) // _Send_ a value into a channel using the `channel <-` // syntax. Here we send `"ping"` to the `messages` // channel we made above, from a new goroutine. go func() { messages <- "ping" }() // The `<-channel` syntax _receives_ a value from the // channel. Here we'll receive the `"ping"` message // we sent above and print it out. msg := <-messages fmt.Println(msg) }

 

上面示例的程序中,我们创建了一个不带缓冲的string类型Channel,然后在一个goroutine把“ping”用channel<-传递给了这个Channel。<-channel收到了这个值,然后在main函数打印出来。

其实在这里我们利用Channel悄然把“ping”message从一个goroutine转移到了main goroutine,实现了线程间(准确的说,goroutine之间的)通信。 因为channel发送方和接收方会一直阻塞直到对方准备好发送或者接收,这就使得我们在程序末尾等”ping” message而无需其他的同步操作。

 

Range

for …… range语句可以处理Channel。

  1. func main() {
  2. go func() {
  3. time.Sleep(1 * time.Hour)
  4. }()
  5. c := make(chan int)
  6. go func() {
  7. for i := 0; i < 10; i = i + 1 {
  8. c <- i
  9. }
  10. close(c)
  11. }()
  12. for i := range c {
  13. fmt.Println(i)
  14. }
  15. fmt.Println("Finished")
  16. }

range c产生的迭代值为Channel中发送的值,它会一直迭代直到channel被关闭。上面的例子中如果把close(c)注释掉,程序会一直阻塞在for …… range那一行。

 

select

select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。

它的case可以是send语句,也可以是receive语句,亦或者default。

receive语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。

最多允许有一个default case,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。

  1. import "fmt"
  2. func fibonacci(c, quit chan int) {
  3. x, y := 0, 1
  4. for {
  5. select {
  6. case c <- x:
  7. x, y = y, x+y
  8. case <- quit:
  9. fmt.Println("quit")
  10. return
  11. }
  12. }
  13. }
  14. func main() {
  15. c := make(chan int)
  16. quit := make(chan int)
  17. go func() {
  18. for i := 0; i < 10; i++ {
  19. fmt.Println(<-c)
  20. }
  21. quit <- 0
  22. }()
  23. fibonacci(c, quit)
  24. }

如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default去处理,如果default case存在的情况下。如果没有default case,则select语句会阻塞,直到某个case需要处理。

需要注意的是,nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。

select语句和switch语句一样,它不是循环,它只会选择一个case来处理,如果想一直处理channel,你可以在外面加一个无限的for循环:

  1. for {
  2. select {
  3. case c <- x:
  4. x, y = y, x+y
  5. case <-quit:
  6. fmt.Println("quit")
  7. return
  8. }
  9. }

 

Timeout

select有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。

下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1。

  1. import "time"
  2. import "fmt"
  3. func main() {
  4. c1 := make(chan string, 1)
  5. go func() {
  6. time.Sleep(time.Second * 2)
  7. c1 <- "result 1"
  8. }()
  9. select {
  10. case res := <-c1:
  11. fmt.Println(res)
  12. case <- time.After(time.Second * 1):
  13. fmt.Println("timeout 1")
  14. }
  15. }

其实它利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。

 

close

内建的close方法可以用来关闭channel。

总结一下channel关闭后sender的receiver操作。

如果channel c已经被关闭,继续往它发送数据会导致panic: send on closed channel:

  1. import "time"
  2. func main() {
  3. go func() {
  4. time.Sleep(time.Hour)
  5. }()
  6. c := make(chan int, 10)
  7. c <- 1
  8. c <- 2
  9. close(c)
  10. c <- 3
  11. }

但是从这个关闭的channel中不但可以读取出已发送的数据,还可以不断的读取零值:

  1. c := make(chan int, 10)
  2. c <- 1
  3. c <- 2
  4. close(c)
  5. fmt.Println(<-c) //1
  6. fmt.Println(<-c) //2
  7. fmt.Println(<-c) //0
  8. fmt.Println(<-c) //0

但是如果通过range读取,channel关闭后for循环会跳出:

  1. c := make(chan int, 10)
  2. c <- 1
  3. c <- 2
  4. close(c)
  5. for i := range c {
  6. fmt.Println(i)
  7. }

通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。

  1. c := make(chan int, 10)
  2. close(c)
  3. i, ok := <-c
  4. fmt.Printf("%d, %t", i, ok) //0, false

 

错误处理之error

Golang的错误处理是一个被大家经常拿出来讨论的话题(另外一个是泛型) 。

Golang中引入error接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含error。error处理过程类似于C语言中的错误码,可逐层返回,直到被处理。

 

error基本用法

error 类型实际上是抽象了 Error() 方法的 error接口,Golang使用该接口进行标准的错误处理。error对应源代码如下:

type error interface { Error() string }

 

这个设计也正好体现了Go哲学中的“正交”理念:error context与error类型的分离。无论error context是int、float还是string或是其他,统统用error作为返回值类型即可。

一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个(但这并非是强制要求)。

我们来用一个开方函数简单示范下 Go 语言中 error 的用法:

package main import ( "errors" "fmt" "math" ) func Sqrt(f float64) (float64, error) { if f < 0 { return -1, errors.New("math: square root of negative number") } return math.Sqrt(f) , nil } func main(){ result, err:= Sqrt(-13) if err != nil { fmt.Println(err) }else{ fmt.Println(result) } }

 

以上是 error 的使用方法:使用errors.New 可返回一个错误信息。与其他语言的异常相比,Golang 的方法相对更加容易、直观。

 

error进阶用法

除了上面的errors.New 用法之外,我们也可以实现error接口,自己实现Error() 方法,来达到自定义参数的错误输出。示例代码如下:

package main import ( "fmt" "math" ) type dualError struct { Num float64 problem string } func (e dualError) Error() string { return fmt.Sprintf("Wrong!!!,because \"%f\" is a negative number",e.Num) } func Sqrt(f float64) (float64, error) { if f < 0 { return -1, dualError{Num:f} } return math.Sqrt(f) , nil } func main(){ result, err:= Sqrt(-13) if err != nil { fmt.Println(err) }else{ fmt.Println(result) } }

 

error类型是go语言的一种内置类型,使用的时候不用特定去import,他本质上是一个接口,

type error interface{ Error() string //Error()是每一个订制的error对象需要填充的错误消息,可以理解成是一个字段Error }

怎样去理解这个订制呢?

我们知道接口这个东西,必须拥有它的实现块才能调用,放在这里就是说,Error()必须得到填充,才能使用.

比方说下面三种方式:

第一种:通过errors包去订制error

error := errors.New("hello,error")//使用errors必须import "errors"包 if error != nil { fmt.Print(err) }

 

来解释一下errors包,只是一个为Error()填充的简易封装,整个包的内容,只有一个New方法,可以直接看

func New(text string) error

 

Timer和Ticker

 

 

 

Panic

1、内建函数

2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行

3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行

4、直到goroutine整个退出,并报告错误

 

go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理

在一个主进程,多个go程处理逻辑的结构中,这个很重要,如果不用recover捕获panic异常,会导致整个进程出错中断

 

Recover

1、内建函数

2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为

3、一般的调用建议

a). 在defer函数中,通过recever来终止一个gojroutine的panicking过程,从而恢复正常代码的执行

b). 可以获取通过panic传递的error

 

 

golang中的rpc包用法

RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样。

我所在公司的项目是采用基于Restful的微服务架构,随着微服务之间的沟通越来越频繁,就希望可以做成用rpc来做内部的通讯,对外依然用Restful。于是就想到了golang标准库的rpc包和google的grpc。

这篇文章重点了解一下golang的rpc包。

 

介绍

golang的rpc支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。

Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:

  • 函数必须是导出的(首字母大写)
  • 必须有两个导出类型的参数,
  • 第一个参数是接收的参数,第二个参数是返回给客- 户端的参数,第二个参数必须是指针类型的
  • 函数还要有一个返回值error

举个例子,正确的RPC函数格式如下:

func (t *T) MethodName(argType T1, replyType *T2) error

T、T1和T2类型必须能被encoding/gob包编解码。

 

示例

举一个http的例子。

下面是http服务器端的代码:

package main import ( "errors" "net" "net/rpc" "log" "net/http" ) type Args struct { A, B int } type Quotient struct { Quo, Rem int } type Arith int func (t *Arith) Multiply(args *Args, reply *int) error { *reply = args.A * args.B return nil } func (t *Arith) Divide(args *Args, quo *Quotient) error { if args.B == 0 { return errors.New("divide by zero") } quo.Quo = args.A / args.B quo.Rem = args.A % args.B return nil } func main() { arith := new(Arith) rpc.Register(arith) rpc.HandleHTTP() l, e := net.Listen("tcp", ":1234") if e != nil { log.Fatal("listen error:", e) } http.Serve(l, nil) }

简单分析一下上面的例子,先实例化了一个Arith对象arith,然后给arith注册了rpc服务,然后把rpc挂载到http服务上面,当http服务打开的时候我们就可以通过rpc客户端来调用arith中符合rpc标准的的方法了。

请看客户端的代码:

package main import ( "net/rpc" "log" "fmt" ) type Args struct { A, B int } type Quotient struct { Quo, Rem int } func main() { client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234") if err != nil { log.Fatal("dialing:", err) } // Synchronous call args := &Args{7,8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil { log.Fatal("arith error:", err) } fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) // Asynchronous call quotient := new(Quotient) divCall := client.Go("Arith.Divide", args, quotient, nil) replyCall := <-divCall.Done // will be equal to divCall if replyCall.Error != nil { log.Fatal("arith error:", replyCall.Error) } fmt.Printf("Arith: %d/%d=%d...%d", args.A, args.B, quotient.Quo, quotient.Rem) // check errors, print, etc. }

简单说明下,先用rpc的DialHTTP方法连接服务器端,调用服务器端的函数就要使用Call方法了,Call方法的参数和返回值已经很清晰的表述出rpc整体的调用逻辑了。

我们把服务器端跑起来,再把客户端跑起来,这时候客户端会输出:

Arith: 7*8=56 Arith: 7/8=0...7

到此,整个rpc的调用逻辑就完成了。

 

 

 

 

 

======================================================================================================================

 

 

 

 

GO与C比较:指针函数返回局部变量地址的不同

 

C语言函数中如何返回变量与指针?

在C语言中,一个函数可以直接返回函数中定义的局部变量,其实在函数返回后,局部变量是被系统自动回收的,因为局部变量是分配在栈空间,那为什么还可以返回局部变量,其实这里返回的是局部变量的副本(拷贝)。

#include <stdio.h> int fun() { int a = 10; return a; //返回的是a的副本 } int main() { int b; b = fun(); printf("%d\n", b); } $ ./a.out 10

 

如果将上面这个例子改成下面:

#include <stdio.h> int *fun() //指针函数 (返回值是一个地址) { int a = 10; return &a; //返回变量a的地址 } int main() { int *b = NULL; b = fun(); printf("%d\n", *b); } 编译时会有警告: main.c:14:12: warning: function returns address of local variable [-Wreturn-local-addr] $ ./a.out Segmentation fault (core dumped) //运行发生段错误

 

这里之所以发生段错误,是因为函数返回后,系统自动回收了函数里定义的局部变量,所以运行时去访问一个被系统回收后的地址空间,一定就会发生段错误,这是C/C++语言的特点。当然,也有办法让其不产生段错误,前面说过,函数里的给变量分配的空间是在栈上面,只要让其实间变为堆上,这样函数返回后系统就不会回收。办法是有的,就是用关键字static声明一下,于是将上面程序改为如下:

#include <stdio.h> int *fun() //指针函数 (返回值是一个地址) { static int a = 10; return &a; //返回变量a的地址,此时的地址是在堆上,即使fun返回,这个地址空间还在堆上,系统不会回收,直到程序退出。 } int main() { int *b = NULL; b = fun(); printf("%d\n", *b); } $./a.out 10

 

GO函数中返回变量,指针

下面先看一个例子:

package main import "fmt" func fun() *int { //int类型指针函数 var a := 1 return &a //这里也返回了局部变量a的地址 } func main() { var p *int p = fun() fmt.Printf("%d\n", *p) //这里不会像C,返回段错,而是成功返回变量V的值1 } $go run main.go 1

这里就是GO与C/C++不同的地方,GO中没有关键字static,但有比static还灵活的用法,下面来看下官方的解释:

When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors.

 

以上的解释说明如下:

GO编译器会自动检查函数中的局部变量,在函数外面有没有人使用,如果有人使用,编译器会将局部变量放到GO的垃圾回收堆上,也就是说,将局部变量升级为堆上的变量,这与C中的static是一回事,但GO比C强大,这个变量你不用担心它会不会导致memory leak,因为GO有强大的垃圾回收机制。

 

 

GO 中线程同步的几种方式

 

golang线程同步WaitGroup简介

WaitGroup用于线程同步,WaitGroup等待一组线程集合完成,才会继续向下执行。 主线程(goroutine)调用Add来设置等待的线程(goroutine)数量。 然后每个线程(goroutine)运行,并在完成后调用Done。 同时,Wait用来阻塞,直到所有线程(goroutine)完成才会向下执行。

对官方的代码做简单修改:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. func main() {
  9. var wg sync.WaitGroup
  10. var urls = []string{
  11. "http://www.golang.org/",
  12. "http://www.google.com/",
  13. "http://www.somestupidname.com/",
  14. }
  15. for _, url := range urls {
  16. // Increment the WaitGroup counter.
  17. wg.Add(1)
  18. go func(url string) {
  19. // Launch a goroutine to fetch the URL.
  20. defer wg.Done()
  21. // Fetch the URL.
  22. fmt.Println(url)
  23. }(url)
  24. }
  25. // Wait for all goroutines to finish.
  26. wg.Wait()
  27. fmt.Println("Game Over")
  28. }

 

执行结果为:

  1. http://www.somestupidname.com/
  2. http://www.golang.org/
  3. http://www.google.com/
  4. Game Over

可以看出3个线程(goroutine)全部执行完成后,wg.Wait()才停止等待,程序继续往下执行,输出Game Over。

 

使用channel 进行同步

func main() {

done := make(chan bool)

ticker := time.NewTicker(time.Millisecond * 1000)

go func() {

total := 0

for t := range ticker.C {

fmt.Println("Tick at:", t)

total += 1

if total > 10 {

break

}

}

done <- true

}()

<-done

ticker.Stop()

}

 

Golang Cond同步机制

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var locker = new(sync.Mutex)
  8. var cond = sync.NewCond(locker)
  9. var waitgroup sync.WaitGroup
  10.  
  11. func test(x int) {
  12. cond.L.Lock()
  13. //等待通知,阻塞在此
  14. cond.Wait()
  15. fmt.Println(x)
  16. time.Sleep(time.Second * 1)
  17. defer func() {
  18. cond.L.Unlock()//释放锁
  19. waitgroup.Done()
  20. }()
  21. }
  22.  
  23. func main() {
  24. for i := 0; i < 10; i++ {
  25. go test(i)
  26. waitgroup.Add(1);
  27. }
  28. fmt.Println("start all")
  29. time.Sleep(time.Second * 1)
  30. // 下发一个通知给已经获取锁的goroutine
  31. cond.Signal()
  32. time.Sleep(time.Second * 1)
  33. //下发广播给所有等待的goroutine
  34. fmt.Println("start Broadcast")
  35. cond.Broadcast()
  36. waitgroup.Wait()
  37. }

 

 

gRPC golang开发指南,官方实例讲解

 

https://blog.csdn.net/wdy_yx/article/details/74203178

 

 

GoLang 包引用规则和方法

 

Go--包引用介绍

  最近在学习Go编程,本文简单的叙述如何在Go编程中使用包(包管理)。

  和其他大多数语言一样,Go也存在包,并使用package关键字定义一个包。首先介绍在程序中如何引入包,引入包有以下几种方式:

  1. 最简单的方式引入一个包的方式是直接引入包,例如:

    import "fmt"

    import "os"

  2. 也可以通过下面的方式将包一块引入,并写在括号内:

    inport (

      "fmt"

      "os"

    )

  通过上面的方式,可以引入系统包或第三方的包,下面重点介绍如何引入自定义的包和函数:

  一般我们将主程序放在src的mian文件夹下(主程序中包含main函数,并将主程序的包名写为package main),将其他模块放在相应的文件夹下,例如下图所示

  

 

  主函数在main.go文件中,主函数名也可以为其他,但必须包含main函数。在Go编程中,怎么引入自己编写的模块呢,例如在main.go中如何调用add.go、subtract.go或者是multiply.go中的文件。

  add.go与subtract.go在cal文件夹下,所以这两个程序的包名为cal(package cal),multiply.go在multi文件夹下,所以程序的包名为multi(package multi)。如果mian函数要调用add.go或者subtract.go中的函数,必须要引入包"cal"(import "cal")。要调用multiply.go中的函数,那就要引入包"multi",如果我们在程序中直接写import "multi",编译器会提示我们can not find package "multi"。因为我们的"multi"包在包"cal"下,所以我们要把包名写完整"cal/multi",下面就可以调用各个文件中的函数了。

  Go中如果函数名的首字母大写,表示该函数是公有的,可以被其他程序调用,如果首字母小写,该函数就是是私有的,因此我们只能调用add.go、subtract.go或者multiply.go中的公有函数。具体调用如下图:

 

add.go

  

 

  subtract.go

 

 

multiply.go

  

 

最后注意:文件名可以和该包名不一致,但文件中使用的包名必须要和该包名一致。

 

 

GO中调用C 库

1: https://blog.csdn.net/linuxandroidwince/article/details/78733366

2: 在Go函数中调用c动态库

Mark_Zhang · 2017-07-01 07:07:56 · 578 次点击 · 预计阅读时间 2 分钟 · 4分钟之前 开始浏览

这是一个创建于 2017-07-01 07:07:56 的文章,其中的信息可能已经有所发展或是发生改变。

在很多场景下,在Go的程序中需要调用c函数或者是用c编写的库(底层驱动,算法等,不想用Go语言再去造一遍轮子,复用现有的c库)。

那么该如何调用呢?Go可是更好的C语言啊,当然提供了和c语言交互的功能,称为Cgo!

Cgo封装了#cgo伪c文法,参数CFLAGS用来传入编译选项,LDFLAGS来传入链接选项。这个用来调用非c标准的第三方c库。

1)先从最简单的写起吧,Go代码直接调用c函数,下面的示例中在代码注释块调用了标准的c库,并写了一个c函数(本例只是简单打印了一句话,在该注释块中可以写任意合法的c代码),在Go代码部分直接调用该c函数hi()

package main import "fmt" /* #include <stdio.h> void hi() { printf("hello world!\n"); } */ import "C" //这里可看作封装的伪包C, 这条语句要紧挨着上面的注释块,不可在它俩之间间隔空行! func main() { C.hi() fmt.Println("Hi, vim-go") }

运行结果:

root@slave2:/home/cgo# go run main.go hello world! Hi, vim-go

好,我可以在Go代码中写c代码了,那么我该如何在Go中直接调用已经编译好的第三方c库呢?用Cgo!

2)本例示范在Go代码中调用非标准的c的第三方动态库

c文件

/* * hi.c * created on: July 1, 2017 * author: mark */ #include <stdio.h> void hi() { printf("Hello Cgo!\n"); }

h文件

void hi();

编译成动态库.so

root@slave2:/home/cgo# gcc -c -fPIC -o hi.o hi.c root@slave2:/home/cgo# gcc -shared -o libhi.so hi.o

Go文件

package main import "fmt" /* #cgo CFLAGS: -I./ #cgo LDFLAGS: -L./ -lhi #include "hi.h" //非标准c头文件,所以用引号 */ import "C" func main() { C.hi() fmt.Println("Hi, vim-go") }

重点来了(敲黑板):

CFLAGS中的-I(大写的i) 参数表示.h头文件所在的路径

LDFLAGS中的-L(大写) 表示.so文件所在的路径 -l(小写的L) 表示指定该路径下的库名称,比如要使用libhi.so,则只需用-lhi (省略了libhi.so中的lib和.so字符,关于这些字符所代表的具体含义请自行google)表示。

运行结果:

root@slave2:/home/cgo# go run main.go Hello Cgo! Hi, vim-go

更深入一些,

1)头文件路径和库文件路径写死的话,一旦第三方库的安装路径变化了,Golang的代码也要跟着变化,就会很麻烦。这时可以使用cgo命令中使用pk-config,具体请参考这篇博文:Golang使用pkg-config自动获取头文件和链接库的方法

2)当在Go中使用了以上的方法后,就要求主机(或者云服务器)上必须有相应的.so文件,如果不存在就会链接报错,导致程序退出。

若.so是一些不必要的第三方驱动库(可有可无),那就麻烦了,你不能为了跑这个程序,把每台主机都装上那个不必要的第三方库吧。有没有一种方法可以在Go程序运行时才调用这些.so库呢,如果不存在忽略就好(就不启用那个库提供的功能了,而不是链接报错直接异常退出)?当然有!敬请期待一下一篇。:)

 

3:Go调用C库的代码:

1 package main 2 3 /* 4 #cgo CFLAGS: -I. 5 #cgo LDFLAGS: -L. -lfoo 6 #include <stdio.h> 7 #include <stdlib.h> 8 #include "foo.h" 9 */ 10 import "C" 11 import "unsafe" 12 import "fmt" 13 14 func Prin(s string) { 15 cs := C.CString(s) 16 defer C.free(unsafe.Pointer(cs)) 17 C.fputs(cs, (*C.FILE)(C.stdout)) 18 //C.free(unsafe.Pointer(cs)) 19 C.fflush((*C.FILE)(C.stdout)) 20 } 21 22 func main() { 23 fmt.Println("vim-go") 24 fmt.Printf("rannum:%x\n", C.random()) 25 Prin("Hello CC") 26 fmt.Println(C.Num) 27 C.foo() 28 }

 

4. C语言和go语言之间的交互操作方法(https://www.cnblogs.com/sevenyuan/p/4544294.html

在Go中可以用如下方式访问C原生的数值类型:

C.char,

C.schar (signed char),

C.uchar (unsigned char),

C.short,

C.ushort (unsigned short),

C.int, C.uint (unsigned int),

C.long,

C.ulong (unsigned long),

C.longlong (long long),

C.ulonglong (unsigned long long),

C.float,

C.double

 

字符串类型

C语言中并不存在正规的字符串类型,在C中用带结尾'\0'的字符数组来表示字符串;而在Go中,string类型是原生类型,因此在两种语言互操作是势必要做字符串类型的转换。

通过C.CString函数,我们可以将Go的string类型转换为C的"字符串"类型,再传给C函数使用。就如我们在本文开篇例子中使用的那样:

s := "Hello Cgo\n" cs := C.CString(s) C.print(cs)

 

数组类型

C语言中的数组与Go语言中的数组差异较大,后者是值类型,而前者与C中的指针大部分场合都可以随意转换。目前似乎无法直接显式的在两者之间进行转型,官方文档也没有说明。但我们可以通过编写转换函数,将C的数组转换为Go的Slice(由于Go中数组是值类型,其大小是静态的,转换为Slice更为通用一些),下面是一个整型数组转换的例子:

 

// int cArray[] = {1, 2, 3, 4, 5, 6, 7}; func CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []int) { p := uintptr(cArray) for i :=0; i < size; i++ { j := *(*int)(unsafe.Pointer(p)) goArray = append(goArray, j) p += unsafe.Sizeof(j) } return } func main() { … … goArray := CArrayToGoArray(unsafe.Pointer(&C.cArray[0]), 7) fmt.Println(goArray) }

执行结果输出:[1 2 3 4 5 6 7]

这里要注意的是:Go编译器并不能将C的cArray自动转换为数组的地址,所以不能像在C中使用数组那样将数组变量直接传递给函数,而是将数组第一个元素的地址传递给函数。

 

枚举(enum)

// enum color { // RED, // BLUE, // YELLOW // }; var e, f, g C.enum_color = C.RED, C.BLUE, C.YELLOW fmt.Println(e, f, g)

输出:0 1 2

对于具名的C枚举类型,我们可以通过C.enum_xx来访问该类型。如果是匿名枚举,则似乎只能访问其字段了

 

Go支持多返回值,而C中并没不支持。因此当将C函数用在多返回值的调用中时,C的errno将作为err返回值返回

 

Go中嵌入C代码,并且使用了export时 重定义的解决办法: https://blog.csdn.net/wangshubo1989/article/details/77451065? locationNum=5&fps=1

 

GO中使用共享内存

https://studygolang.com/articles/10203

 

 

GO使用海思工具链交叉编译

GOARCH=arm

GOARM=7

GOOS=linux

CGO_ENABLED=1

CC=arm-hisiv500-linux-gcc

CXX=arm-hisiv500-linux-g++

 

Alpha 在图像中到底是什么?

https://www.cnblogs.com/suogasus/p/5311264.html

 

GO 多进程编译问题

go 编译采用静态编译,多进程时如何解决单个可执行文件过大的问题?

 

1. 将go标准库编译成动态库,编译时采用动态链接。方法如下:

http://reborncodinglife.com/2018/04/29/how-to-create-dynamic-lib-in-golang/

https://blog.csdn.net/linuxandroidwince/article/details/78723441

问题: 1.10版本编译后的动态库 X86大小在30M, arm没有编译成功。标准库各个包之间存在依赖关系,很难裁剪。(GOARCH=arm GOARM=7 go build -buildmode=shared -linkshared std )

 

将系统库函数编译生动态库: go install -buildmode=shared -linkshared std

成功编译后会在$GOROOT目录下生标准库的动态库文件libstd.so,一般位于$GOROOT/pkg/linux_amd64_dynlink目录:

 

动态链接编译程序: go build -linkshared xx.go

【链接动态库 将程序编译成动态库:go install -buildmode=shared -linkshared demo】

【将程序编译成动态库:go install -buildmode=c-shared -o libxx.so xx.go 【问题:C引用go 静态库时使用uclibc 会出现程序段错误,解决方法:https://go- review.googlesource.com/c/go/+/37868】,这种方式的修改,不能使用flag 包,否则会产生错误】

runtime1.go

func args(c int32, v **byte) {

argc = c

argv = v

if isarchive {

argc = 0

args := unsafe.Pointer(persistentalloc(sys.PtrSize*4, 0, &memstats.other_sys))

// argv pointer

*(**byte)(args) = (*byte)(add(args, sys.PtrSize*1))

// argv data

*(**byte)(add(args, sys.PtrSize*1)) = (*byte)(nil) // end argv

*(**byte)(add(args, sys.PtrSize*2)) = (*byte)(nil) // end envp

*(**byte)(add(args, sys.PtrSize*3)) = (*byte)(nil) // end auxv

argv = (**byte)(args)

println("call args")

}

sysargs(argc, argv)

}

 

【代码编译成静态库: go build -x -v -ldflags "-s -w" -buildmode=c-archive -o main.a main.go】

【GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-hisiv500-linux-gcc go build】

http://www.cnblogs.com/coolyylu/p/7008074.html

https://www.cnblogs.com/majianguo/p/7491508.html

 

使用加壳软件upx 将 elf 或者so 文件压缩: https://my.oschina.net/rkd/blog/613958

 

GO 点阵字体创建

http://www.cnblogs.com/ghj1976/p/5260480.html

https://my.oschina.net/wolf2leader/blog/1594976

 

go 中使用 自带编码转换: https://blog.csdn.net/u011411069/article/details/78755856

go 中使用第三方包将 GBK 转换为 UTF-8

https://studygolang.com/articles/2323

https://blog.csdn.net/jeffrey11223/article/details/79287010

 

GO中内存操作内容

内存的一些常用操作方法:【https://my.oschina.net/chai2010/blog/168484

 

如何将Go 的[]byte 切片类型的数据copy 到C 的内存中:【https://stackoverflow.com/questions/48756732/what-does-1-30c-yourtype-do-exactly-in- cgo

https://grokbase.com/t/gg/golang-nuts/13aprnjk7m/go-nuts-how-to-get-memory-from-c-and-cast-to-byte

func _Cfunc_CBytes(b []byte) unsafe.Pointer {

p := _cgo_cmalloc(uint64(len(b)))

pp := (*[1<<30]byte)(p)

copy(pp[:], b)

return p

}

 

 

https://note.youdao.com/share/?id=0d6ea9b96b77b0489a05c92e6cccad53&type=note#/

 

 

 

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值