1,go
开源,编译性语言,语法简单,并行处理封装。常应用于搭载 Web 服务器。
2,一切皆类型
一个最小可行性编程语言包括对数据结构的支持和对指令流程的支持,可以让程序员实现和操作数据结构,编写指令实现算法。
go语言认为接口/结构体/函数皆类型,字典/数组/指针/切片/通道等都是类型,和int/string/bool等同属于第一公民。(如有不了解的概念,比如切片/通道等可参考后续`类型`章节说明)。
3,类型
类型是对数据结构的定义,包括内建类型,扩展类型和自定义类型。每个类型都有对应的零值。
还可以根据是否是动态类型进行区分:切片,映射,指针,函数,闭包,通道这些是动态类型,其他是非动态类型(区别体现在能否用于映射的健类型)。
还可以根据变量存储内容区分引用类型和值类型,引用类型包括指针,slice,map,chan,变量存储的是一个地址,这个地址存储最终的值,内容通常在堆上分配,通过GC回收,值类型包括int, float,bool, string以及数组和struct,变量直接存储值,内容通常在栈中分配,值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。
还可以根据是否可变对象区分:内建类型都是不可变对象,其他都是可变对象(区别在于修改其类型变量值时是否会重新开辟内存)。还可以根据是否是复合类型区分:bool/int/float/complex/string/rune/error等都是基础类型,array/slice/pointer/map/chan/struct/interface都是复合类型。
3.1 内建类型
不需要使用type关键字定义,直接可以使用的类型。
3.1.1 整数类型
分为无符号整型和有符号整型。
还有一些可以指定类型宽度的整形。
另外,byte是uint8别名,rune是int32的别名,一个rune的类型值即可表示一个Unicode字符。直接打印rune类型是unicode编码对应数字,fmt.Printf("%c", s)可以打印出可读unicode码。
3.1.2 布尔类型
类型名:bool ,取值范围:true/false
3.1.3 浮点类型
类型名:float32/float64。有整数部分/小数点/小数部分组成,另外一种方式是加入指数部分:3.9E-2或者3.9e-2。
3.1.4 复数类型
类型名:complex64和complex128。complex64类型的值会由两个float32类型的值分别表示复数的实数部分和虚数部分。而complex128类型的值会由两个float64类型的值表示复数的实数部分和虚数部分。
3.1.5 字符串类型
类型名:string。字符串的字节默认使用 UTF-8 编码,支持 Unicode 字符。有两种表示法:原生表示法和解释型表示法。
原生表示法,需用用反引号"`"把字符序列包起来,如果用解释型表示法,则需要用双引号"""包裹字符序列。前者所见即所得,后者会对转义字符转义(比如换行符”\n”)。
len("hello,世界")获取的是每个字符的 UTF-8 编码的长度和12,而不是直接的字符数量8。utf8.RuneCountInString("hello,世界") 的结果是 8。
字符串是不可变对象。不能直接修改字符串的某个字符,比如不能s[0] = “X”,只能重新赋值字符串值,原来的值会被内存回收。字符串的每个字符类型都是rune类型,可以使用数组类似的遍历取元素(见下`数组`章节)。
字符串拼接方案:直接“+”会产生新的临时字符串,性能较差;fmt.Sprintf拼接逻辑复杂,性能一般;strings.Join(stringArray, sep),先定义一个字符串数组然后拼接,性能不错;buffer.WriteString性能很好;buffer.Builder官方建议。
3.2 扩展类型
不需要type关键字定义,但是需要基于内置类型扩展支持的类型。
3.2.1 数组类型
类型名:[arrayLength]typeName。只有arrayLength和typeName都完全一样,才属于相同类型。一个数组里的元素,其类型都是typeName。如果是二维数组,相当于typeName又是一个数组类型:[arrayLength][arrayWidth]typeName。
把该类型的类型字面量写在最右边,然后用花括号包裹该值包含的若干元素,各元素之间以(英文半角)逗号分割:[3]int{1,2,3} 或者省略数组长度给出每个元素[...]int{1,2,3}。
可以通过下标访问数组中的某个元素,也可以修改之。
可以遍历数组的每一个元素(for操作可以参考`程序流程`章节):
3.2.2 指针类型
指针类型是描述其他类型地址的类型,类型名:*typeName。如果是指针类型的指针类型,就需要:**typeName。
指针类型涉及取指针操作和取目标操作,操作符&+目标对象,获取对应的一个指针类型,操作符*+指针对象,获取指针对象指向地址的目标。内建类型和函数类型以及指针类型取指针,可以直接fmt.Println(&varName)打印出一个十六进制的地址值,扩展类型和自定义类型可以打印出结构体说明。同时所有类型都可以通过fmt.Printf(“%p”, &varName)都可以打印出十六进制地址值。
3.2.3 切片类型
切片(slice)是对数组的一个连续片段的引用,切片的内部结构包含地址、大小len和容量cap,类型名:[]typeName。
切片可以从数组引用,arrayVar[start:end]就是一个切片,是arrayVar的连续片段的引用,其len=end-start, cap=len(arrayVar)-start。修改切片里的元素,如果没有发生容量扩增,会影响到被引用数组的值,如果发生了容量扩展,切片另辟空间,后续改动不会影响引用数组。
切片支持make操作:make( []Type, size, cap ),指定type/len/cap,分配内存并对每个元素赋值零值(注意不是对切片赋值零值)。
切片支持append操作:需要注意,如果append后,cap不足会触发扩容,扩容前切片元素变化会修改源数组,扩容后不会。append还可以实现删除某个元素的操作:append(sliceName[:1], sliceName[2:]...)。
切片支持copy操作:copy( destSlice, srcSlice []T) int,返回发生复制的元素个数。复制后,两个切片不互相影响。
切片支持range,和数组一样。
多维切片:[][]...[]sliceType。二维切片和二维数组相比,前者每个元素要求是一维切片就行(不需要确定元素数),后者要求每个元素也要是元素数确定的一维数组。
3.2.4 映射类型
是一个k-v为元素的无序集合,类似于python的dict结构,类型名:map[keyType]valueType。可以动态增长。声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的。
映射的key必须是非动态类型,不能是切片,映射,指针,函数。
映射支持len操作,获取元素对的数量。
支持make操作,返回一个零个元素对的集合,可以指定cap容量,但是不支持cap函数操作。
可以对不存在的key取value,mapVarName[keyName]如果不存在keyName返回对应valueType的零值。
可以使用结构体作为keyType,实现多键索引。
可以遍历每个元素对(见下面的`流程控制循环流程`)。
不可对映射类型的零值(见下面的`零值说明`章节)进行k指定赋值v的操作,所以不建议只声明不初始化一个映射类型(见下面的`零值说明`章节),也不建议使用new创建映射类型指针(见下面的`指针类型特殊声明`章节)。
不可对映射的value取地址:map中的元素并不是一个变量,而是一个值,所以map的value和用来赋值的变量不连动。
可以定义valueType是一个切片类型,以此实现value是不确定数量的数据。
映射可以自动扩容,自动缩容。
不可对零值的map进行kv赋值(参考下面的`零值`章节)。
映射支持delete操作:delete(mapVarName, keyName)。如果想一次删除所有的kv对,直接重新make一个映射即可(不用关心内存回收,详细参考下面的`内存管理`章节)。
映射不是线程安全的,使用的时候需要加锁(参考`并发`章节),一般建议使用通道类型(参考`通道类型`章节)。
map是无序的。map会动态扩容,但是map扩容后map变量存储的地址是不变的,而slice扩容后slice变量存储的地址是变的。
map的value都是常量值,可以修改value为另一个常量值,但是不能修改这个常量的值。如果想修改value的值,需要通过存储value为类型指针,通过指针修改。
package main
import "fmt"
type Student struct {
Name string
}
var list map[string]Student
func main() {
list = make(map[string]Student)
student := Student{"Aceld"}
list["student"] = student
student.Name = "LDB"
list["student"] = student
fmt.Printf("%p", &student)
fmt.Printf("%p", &list["student"]) // 报错,无法取常量的地址,见下面的常量章节
list["student"].Name = "abc" // 报错,无法修改value的值
fmt.Println(list["student"])
}
3.2.5 通道类型
Go语言提倡使用通信的方法代替共享内存。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据,遵循先入先出(First In First Out)的规则。goroutine 间通过通道就可以通信(goroutine是go并发协程概念,可参考下面的`协程并发`章节)。变量名:chan typeName;chan<-typeName(单向只写通道);<-chan typeName(单向只写通道)。
channel 是进程内的通信方式,使用 channel 在两个或多个 goroutine 之间传递消息。(参考下面的`协程并发`章节)
支持make创建通道:make(chan 通道类型, 缓冲大小)。如果不指定缓冲大小,就创建一个无缓冲通道。无缓冲通道的话相当于缓冲空间为0,如果通道有数据则不可写;有缓冲通道的话,如果通道中有缓冲空间,就可以继续写。
支持len查询通道中的数据长度,支持cap查询通道的缓冲容量。
向通道发送数据:通道变量 <- 值。
使用通道接收数据:阻塞式接收varName <- chanName,非阻塞接收:varName,ok <-chanName,接收即忽略 <-chanName。
非阻塞的通道接收方法可能造成高的 CPU 占用,如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行(见下面章节的`选择流程`)。select操作:使一个 Go 程可以等待多个通信操作(见下面章节的`选择流程`)。
close操作:只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
range操作:循环 for i := range c 会不断从信道接收值,直到它被关闭(参考下面的`循环流程`)。
3.3 自定义类型
系统不支持的类型,用户可以根据需要使用关键字type自定义的类型。自定义类型都可以是匿名类型,比如匿名接口,匿名接口,匿名结构体,匿名函数(其实type自定义的时候相当于给匿名类型创建了一个别名,见下面的`别名类型`章节)。
3.3.1 别名类型
使用type关键字,对已有的某个类型起个别名。类型定义语法:type myType localType,其中localType可以是其他的任意类型。
别名相当于copy了一份类型结构,并以此新建另一个类型,新类型具有原有类型的所有结构和方法,两个结构互不干涉。
如果想为内建类型和扩展类型增加方法(方法概念见下面`函数`章节),需要首先对相应类型创建别名,然后对别名增加方法。
3.3.2 函数类型
函数是实现一个算法的代码块,接受参数,返回结果。在go语言中函数是第一公民,是一种特殊的类型。函数首先是一个函数:func funcName(paramName paramTypeName) returnTypeName{codes},其次是一种类型,类型定义:type myTypeName func(paramName paramType) returnTypeName。所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
具有接收者的函数,又称为接收者的方法。接收者是一个类型,但是不能是内建类型也不能是扩展类型,必须是本地的自定义类型(包括别名类型)。
函数类型:使用场景一般是为函数类型定义方法,方法里调用函数自身,并增加一些别的操作(实现结构型设计模式里的装饰器模式):
// 函数定义为类型
type FuncCaller func(int)
// 实现Invoker的Call
func (f FuncCaller) Call(p int) {
// 调用f函数本体
f(p)
}
FuncCaller(myFunc) // myFunc是一个可以转换成FuncCaller类型的自定义函数(类型转换见下面的`类型操作`的`类型转换`章节),这样相当于装饰器FuncCaller修饰了myFunc。多个装饰器同时装饰一个函数时,函数前的操作操作顺序是先进先出,函数后的操作顺序是先进后出。
函数是一种类型,所以函数变量的使用方式如下:
var f func() / 声明定义
f = myFuncName /初始化
f() /变量使用(函数变量的使用就是`调用`)
函数内不能定义结构体方法或其他非匿名函数,即不支持嵌套函数声明。如果有需要定义就使用匿名函数,或者类型函数,并以闭包的形式实现。
闭包:一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成闭包才具有“记忆性”。闭包就是一个函数F,函数体中声明一个匿名函数以及变量,匿名函数不通过参数传递,而是直接引用变量并可以修改变量的值,具有匿名函数+引用环境的函数F称为闭包。闭包本质上是“把函数F当作类型去使用”,实例化后可以执行,并把执行结果保存到环境变量中,下次执行同一个实例时,环境变量仍然保存着实例的上次执行结果。
在上述例子中函数adder()内部定义了一个匿名函数,并将这个匿名函数作为返回值,这个匿名函数就是闭包.匿名函数可以定义自己的变量 v,但同时也可以访问adder()内定义的变量 sum ,对于匿名函数来说,它自己定义的变量 v,是属于它的局部变量,而它可以访问的 sum 就是它的全局变量,也就是它所处环境中的变量,我们一般称作"自由变量"。
func adder() func (value int) int{
sum := 0
return func (v int) int{ //返回的匿名函数就是一个闭包,对于闭包,v是局部变量,sum是自由变量,是闭包所处的环境
sum += v
return sum
}
}
func main() {
a1 := adder() //a是adder()返回的匿名函数,就是闭包
a2 := adder()
for i:= 0; i < 5; i++{
fmt.Printf("0+...+%d=%d\n", i,a1(i))
}
for i:= 0; i < 5; i++{
fmt.Printf("0+...+%d=%d\n", i,a2(i))
}
}
上述例子中,可以看到两次调用adder()产生的结果 a1, a2 是隔离的.在实际操作中,每次调用adder()函数,都会分配一个 sum, 同时返回一个可以访问操作 sum 的匿名函数(闭包).其实,调用adder(), 可以理解为实例化了一个"闭包类",在这个实例化的"闭包类"中,有数据域 sum ,和对数据域的操作函数(闭包)。
函数可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。
套用一句经典的话,对象是附有行为的数据,而闭包是附有数据的行为。
原文链接:https://blog.csdn.net/jt102605/article/details/82261775
func F(){
// 准备一个int
v := 1
// 创建一个匿名函数
fmt.Println(v)
foo := func() {
v += 1
}
foo()
fmt.Println(v)
foo()
fmt.Println(v)
}
使用闭包实现斐波那契数列:
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
函数可以支持可变数量的参数,使用三个点定义函数参数:func myFunc(args ...typeName){ for _, arg := range args { fmt.Println(arg) }}。三个点...typeName本质上是切片,必须是最后一个参数。如果是任意类型的可变参数,可以:args ...interface{}。
引用类型作为参数,比如切片,映射,通道类型变量,不必使用指针就可以在函数内修改引用对象的值。非引用类型作为参数,如果是指针传递则函数内可以修改引用对象的值,否则不会修改引用对象的值。
使用time.Since()可以计算函数的执行时间。
3.3.3 接口类型
接口类型 是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。
类型定义:type typeName interface{FuncName1(param1 typeName1, param2 typeName2) returnTypeName} 。
类型定义里只能出现方法声明,不能出现属性声明(方法声明的时候,不需要func关键字,也不需要打括号包裹的方法体)。
Go 语言的接口设计是非侵入式的,无需implent或者其他显式的实现表达式,这样接口的实现可以出现在任何包中。 一个类型可以实现多个接口,而接口间彼此独立,不知道对方的实现。多个类型可以实现相同的接口。
接口可以嵌套组合:
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type WriteCloser interface {
Writer
Closer
}
接口值:接口类型的数据(见下面的`数据`章节),和string值,int值一个概念。只不过接口值保存了一个具体底层类型(可以是任何类型:内置类型,扩展类型,自定义类型)的具体值和类型:(value, type),即:
type I interface {
M()
}
type T struct {
S string
}
var i I / 这里相当于接口值是nil
var t *T
i = t / 这里相当于接口值i保存了具体值t,同时还保存了具体类型T。
接口值调用时,会执行其底层类型同名方法。保存了nil具体值的接口值本身并不是nil(仔细品上一行的赋值过程)。
空接口可以保存任何类型的值:interface{},因为每个类型都至少实现了零个方法。
接口值要么是nil,要么是某个实现这个接口的类型值,不能直接使用接口实例化:
var i interface{} / 空接口类型,接口值初始化为nil,不能通过interface{}实例化别的值
i = 1 / 然后赋值任意类型,如果赋值的类型没有实现接口,会编译失败。
接口类型断言:t:=i.(T)。该语句断言接口值 i 保存了具体类型 T,T也可以是一个接口类型,并将其底层类型为 T 的值赋予变量 t,所以i一定要是接口值。t, ok := i.(T)这样写可以通过ok返回断言是否成功,并防止返回panic。
类型选择:类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。
switch v := i.(type) {
case T:
// i 的类型为 T
case S:
// i 的类型为 S
default:
// 没有匹配
} // 最终v都会赋值i
fmt包定义了
type Stringer interface {
String() string
}
fmt.Println/Printf都是接收一个Stringer接口类型,然后打印的时候调用接口值保存的底层值的String()方法。所以可以为任何类型定义String方法,然后就可以使用fmt.Println/Printf打印。
type error interface {
Error() string
} // 所以任何实现了Error方法的类型,其类型的值都可以当作error接口值。
3.3.4 结构体类型
一个结构体(struct)就是一组字段(field),每个字段都是结构体的成员。字段名有自己的类型,字段名必须唯一,字段类型可嵌套结构体类型,也可以是接口类型。
类型定义语法:
type typeName struct{
fieldName1 typeName1 / 也可以不写fieldName1只使用typeName1,如果是结构体类型可以这么写,否则不建议这么做
fieldName2 typeName2
fieldName3 otherStructTypeName / 可以嵌套结构体
}
实例化的时候才会分配内存(参考下面的`结构体类型声明`章节)。结构体字段通过点号访问,嵌入结构体内部可能拥有相同的成员名,此时先检查自身的字段,后检查嵌套结构体的字段。如果可能存在歧义(比如多个嵌套结构体都有某个字段,不知道优先取哪个嵌套结构体里的字段),编译会出错。
可以定义属于结构体的方法,方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间,此时结构体称为方法的接收者。
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs1() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Abs2(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
} // Abs1和Abs2实现功能是一样的,但是前者是Vertex的方法,后面是一个接收Vertex类型参数的函数。
接受者可以是是结构体类型,也可以是结构体指针类型。指针接收者的方法可以修改接收者指向的值,结构体接收者只会对接收者的副本修改(这点和函数接收的参数类型是否是指针的规则一致)。指针作为接收者,可以避免每次都值复制,会更加高效,是推荐用法。
给定类型的方法都应该有值或指针接收者,但并不应该二者混用:当某一类型的所有方法接收者都是指针时,每个方法都会对变量本身进行修改;如果我们将值接收者和指针接收者混用,那在某一次调用指针接收者的方法后,对于后续的值接收者方法,可能会对本身值产生不应该的修改。
结构体是否能比较:
- 如果两个结构体内部成员类型不同,则不能比较
- 如果两个结构体内部成员类型相同,但是顺序不同,则不能比较
- 如果两个结构体内含有无法比较类型,则无法比较
- 如果两个结构体类型,顺序相同,且不含有无法比较类型(slice,map)则可以进行比较
3.3.5 其他生态包定义的类型
比如container/list包的List类型。
3.4 零值说明
当一个变量或者新值被创建时,如果没有为其明确指定初始值,go语言会自动初始化其值为此类型对应的零值。
整数类型的零值是0,浮点型零值为0.0。
字符串类型的零值是空字符串。
布尔型零值为false。
pointer, channel, map, slice, func, interface类型的零值都是nil,但是打印结果可能不是nil,同时len操作结果返回0。比如:
var m map[string]int
fmt.Println(m, m==nil)
nil 不是关键字。
nil 没有默认类型。
任何地址类型的nil值的都是0x0,就是nil作为地址类型时,其打印的值是0x0。
不同类型的nil内存不同:
unsafe.Sizeof(varNameNilValue)
俩nil 常量不能比较:
fmt.Println(nil==nil) // invalid operation: nil == nil (operator == not defined on nil)
两个不同类型的nil不能比较,两个interface类型或者指针类型的nil变量能比较,其他类型的nil不能比较。
struct类型的零值是所有属性都是零值的结构类型实现。
数组类型的零值是每个元素都是零值的数组。
map类型的零值不可赋值,所以不要只声明不初始化就直接设置kv对:
var mapCreated map[string]float32
mapCreated["key1"] = 1 // panic: assignment to entry in nil map
3.5 类型操作
3.5.1 变量声明
详细可参考`数据`章节。
3.5.2 类型转换
类型转换的表达式语法: typeName(varName)。
如果不能转换,比如把string转int,就会编译不通过。
3.5.3 类型判断
fmt.Printf(“%T”, varName)
或使用反射机制
type Info struct {
Name string `Testing:"_"`
Age int `Testing:"age,min=17,max=60"`
Sex string `fuck:"sex, required" a:"b"`
}
info := Info{
Name: "benben",
Age: 23,
Sex: "male",
}
t := reflect.TypeOf(info)
fmt.Println("Type:", t.Name())
fmt.Println("Kind", t.Kind())
for i:=0; i<t.NumField();i++{
field:= t.Field(i)
tag:=field.Tag.Get("fuck")
fmt.Printf("%d. %v (%v), tag: '%v'\n", i+1, field.Name, field.Type.Name(), tag)
}
或者使用接口断言判断一个接口是否属于某个类型
varName2, ok = varName.(typeName) / varName必须是一个interface类型
或者使用switch判断类型
swith(varName.(type)) /参考下面的`选择流程`章节
3.5.4 定义其他类型
比如别名类型/结构体类型/接口类型/函数类型的定义,都会用到其他系统内置类型或其他自定义类型。
3.6 作用域
内置类型和基础类型在任何地方都可以随时使用,自定义类型可以在定义所在包内的所有源文件中使用,同时可以通过import导入别的包(如果类型名称首字母大写,详情可参考后续的`包管理`章节)后使用其中的类型。
4,数据
4.1 常量
用于存储不会改变的数据,常量是在编译时被创建的。只能是内建类型,比如布尔型、数字型(整数型、浮点型和复数)和字符串型。不能取地址。
4.1.1 数值常量
比如数值:1,2,”a”,’a’
4.1.2 自定义常量
const constName typeName = constValue
const (
constName1 = 1
constName2 // 如果不指定值,会沿用上一个常量的值 1
)
4.1.3常量生成器
type Weekday int // 这里只能使用int,不建议float,不能string等其他
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
4.2 变量
Go语言是静态类型语言,变量(variable)是有明确类型的,编译器会检查变量类型的正确性。在数学概念中,变量表示没有固定值且可改变的数。但从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
4.2.1 标准定义
var varName typeName // 只声明
var varname typeName = 表达式 // 声明 + 初始化
4.2.2 批量格式
var varName1, varName2 typeName
var (
varName1 typeName1
varName2 typeName2
)
4.2.3 简短格式
与标准定义相比,简短格式有几点不同:
- 不能定义代码块(批量定义)
- 必须给出初始值
- 短变量声明是可以重新声明变量
- 简短格式适用局部变量声明
varName := 表达式
varName, _ := 表达式 // 第二个接收变量是匿名变量(没有名称,赋值即抛弃)
4.2.4 指针类型特殊说明
一切指针类型变量,都可以通过new声明并初始化,比如 varName:=new(typeName),会声明一个typeName类型的变量并赋值零值,然后把其指针赋值给varName。
不要new创建map指针变量,因为会出现assignment to entry in nil map的panic(除非new之后,增加冗余的赋值语句,这样的话前面的new语句其实就不用了):
new和make的区别和联系:
- new和make都会分配内存
- new是对指针类型分配内存(当然指针指向的内存也被分配了),返回指定类型的指针值,new不能直接对 slice 、map、channel 分配内存。
- make仅仅用于slice/map/channel的初始化,可以指定容量并直接分配内存。
func new(Type) *Type // new 是 Golang 的内建函数,用于分配内存,其中,第一个参数是类型,返回值是类型的指针,其值被初始化为“零”(类型对应的零值,int 初始化为0,bool初始化为 false 等)。
mapCreated := new(map[string]float32)
(*mapCreated)["key1"] = 4.5 // panic: assignment to entry in nil map
4.2.5 结构体类型特殊说明
ins := 结构体类型名{
字段1: 字段1的值,
字段2: 字段2的值,
…
} // 显示指明字段名:字段值,可以不用按照顺序为字段赋值,也可以只为部分字段赋值。
实例化一个匿名结构体:
msg := &struct { // 定义部分
id int
data string
}{ // 值初始化部分
1024,
"hello",
} // 按照默认字段顺序赋值,必须对所有字段赋值。
构造函数实例化一个结构体:
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name,
}
}
结构体指针类型的变量p = &Cat{“red”, “name”},可以通过 (*p).Color 来访问其字段 X,也可以直接使用点号隐式间接引用获取字段。
4.3 作用域
一个常量/变量,作用域分为局部变量,全局变量,形式参数。
4.3.1 局部变量
函数体内声明的变量称之为局部变量。它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。
4.3.2 全局变量
在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用。其他源文件可以通过import引入(首字母大写的话,后续`包管理`章节会说明)然后使用。
4.3.3 形式参数
在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。
5 流程支持
任何算法都可以有顺序结构/选择结构/循环结构这三种基本流程结构组成。go支持这三种基本结构。
5.1 顺序流程
略。
5.2 选择流程
if表达式外无需小括号 ( ) ,大括号 { } 则是必须的。
if x < 0 {
fmt.Println("if")
}else {
fmt.Println("else")
}
select 语句使一个 Go 程可以等待多个通信操作。select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
select {
case num := <-ch:
fmt.Println("num = ", num)
case <-time.After(3 * time.Second):
fmt.Println("超时")
quit <- true
}
switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。
// 对s进行类型断言
switch s.(type) { // s.(type)只能用于swith后
case bool: // 当s为布尔类型时
typeString = "bool"
case string: // 当s为字符串类型时
typeString = "string"
case int: // 当s为整型类型时
typeString = "int"
}
没有条件的swith,相当于swith true:
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
5.3 循环流程
基本的 for 循环由三部分组成,它们用分号隔开:
初始化语句:在第一次迭代前执行
条件表达式:在每次迭代前求值
后置语句:在每次迭代的结尾执行
for i := 0; i < 10; i++ {
sum += i
} // 初始化和后置语句,可选的
常配合range使用。
遍历数组:
var team [3]string
for k, v := range team {
fmt.Println(k, v)
}
遍历映射:
for k, v := range mapName {}
for k := range mapName {} // 只遍历key
循环接收渠道里的内容
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
循环语句里,修改列表元素要注意,for语句里接收变量和列表元素并不是同一个地址。
package main
import "fmt"
type List struct {
Head *Node
}
type Node struct {
Val int
Next *Node
}
func main() {
fmt.Println("abc")
nodeList := []Node{{Val: 1}, {Val: 2}, {Val: 3}}
for i, node := range nodeList {
fmt.Println(i)
fmt.Printf("%p\n", &node)
fmt.Printf("%p\n", &nodeList[i])
}
}
// 0
// 0xc000010200
// 0xc00005e180
// 1
// 0xc000010200
// 0xc00005e190
// 2
// 0xc000010200
// 0xc00005e1a0
遍历map或list时,for循环里接收item的临时变量,只被定义一次,其地址从未改变。
- 直接使用这个临时变量,最终值都是for循环的最后一个元素的副本。
- 直接修改这个临时变量,不会影响到map或list对象本身。
package main
import "fmt"
type Student struct {
Name string
Age int64
}
var list map[string]Student
func main() {
stus:=[]Student{
{Name:"zhou",Age:24},
{Name:"li",Age:23},
{Name:"wang",Age:22},
}
m:=make(map[string]*Student)
//将数组依次添加到map中
for _,stu:=range stus{
m[stu.Name]=&stu
}
//打印map
// foreach中,stu是结构体的一个拷贝副本,所以m[stu.Name]=&stu实际上一致指向同一个指针,最终该指针的值为遍历的最后一个struct的值拷贝。
for k,v:=range m{
fmt.Println(k,"=>",v.Name)
}
}
// output
//zhou => wang
//li => wang
//wang => wang
5.4 延后流程
defer 语句会将函数推迟到外层函数返回之后执行。推迟的函数调用会被压入一个栈中。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用:
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i) // 真实打印顺序是9,8,7,6…
}
fmt.Println("done")
}
6 其他特性
6.1 注释
go语言使用双斜线// 进行单行注释,也可以使用/*和*/进行多行注释。
6.2 内存管理
6.3 异常处理
go预定义了error接口类型:
type error interface {
Error() string
}
我们在代码中定义了一个error,这个error我们可以不处理,不影响程序继续进行。但是如果定义了一个panic,那程序不会执行下去:
fmt.Print("start....")
panic("an error occured: stopping")
fmt.Print("end")
如果panic函数遇到了defer延迟函数,在defer函数中触发了panic函数异常,会将该异常一直往上携带,一直输送到这个协程的起点。
recover内建函数被用于从 panic 或 错误场景中恢复。必须的在defer修饰的方法中使用,不然不生效。
package main
import (
"fmt"
"log"
)
func defer1() {
panic("an error occured: stopping")
fmt.Println("defer1")
}
func defer2() {
defer func() {
if err := recover();err != nil{
log.Printf("panic: v%",err)
}
}()
defer1()
fmt.Println("defer2")
}
func main() {
fmt.Println("start....")
defer2()
fmt.Println("end")
}
6.4 协程并发
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
goroutine其实就是线程,是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。
使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine:
go 函数名( 参数列表 ) // 普通函数创建goroutine
go func( 参数列表 ){ // 使用匿名函数创建goroutine
函数体
}( 调用参数列表 )
channel 是Go语言在语言级别提供的 goroutine 间的通信方式。
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行。
可以设置GOMAXPROCS环境变量,或者runtime.GOMAXPROCS(逻辑CPU数量)修改使用的cpu数量并返回上一次设置的cpu数量:GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting。所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程,接下来协程会被分割(分散)到 n 个处理器上。
有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。
goroutine 可能发生并行执行,goroutine 可能发生在多线程环境下,goroutine通过通道来通信,这是和coroutine的区别。
runtime.Gosched()可以让出cpu。
主协程等待子协程结束后再结束:
func main() {
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
wg.Add(2) // 因为有两个动作,所以增加2个计数
go func(x int) {
a, b := 1, 1
for i := 0; i< x;i++{
fmt.Println(a)
a, b = b, a+b
}
wg.Done() // 操作完成,减少一个计数,用到了闭包
}(100)
go func(x int) {
a, b := 1, 1
for i := 0; i< x;i++{
fmt.Println(a)
a, b = b, a+b
}
wg.Done() // 操作完成,减少一个计数
}(100)
time.Sleep(3)
myfib(100)
wg.Wait()
}
还可以通过渠道实现:
func main() {
runtime.GOMAXPROCS(1)
ch := make(chan struct{})
count := 2 // count 表示活动的协程个数
go func(x int) {
fmt.Println("Goroutine 1")
a, b := 1, 1
for i := 0; i< x;i++{
fmt.Println(a)
a, b = b, a+b
}
ch <- struct{}{} // 协程结束,发出信号
}(100)
go func(x int) {
fmt.Println("Goroutine 2")
a, b := 1, 1
for i := 0; i< x;i++{
fmt.Println(a)
a, b = b, a+b
}
ch <- struct{}{} // 协程结束,发出信号
}(100)
for range ch {
// 每次从ch中接收数据,表明一个活动的协程结束
count--
// 当所有活动的协程都结束时,关闭管道
if count == 0 {
close(ch)
}
}
}
6.5 包管理
包管理/mod
每个go程序都由包构成的,程序从main包的main函数开始运行。编译不包含main包的源文件后,不会得到可执行文件。
相同包的源程序共享类型定义和变量,无论是否首字母大写。
不同包的源程序需要通过import关键字导入别的包,进而使用别的包里定义的首字母大写的类型定义和变量。
import 的是目录,建议绝对路径:import “包的路径”。
多行导入:
import (
“包1的路径”
“包2的路径”
)
import F “fmt”// 别名
import . “fmt”// 省略引用,可以直接使用fmt里的变量或类型TypeName,不需要fmt.TypeName
import _ “fmt” // 匿名引用,只执行包内的init函数,不使用包内的数据。
同一个目录下的源文件应该属于同一个包。
包名和目录名没有关系,但是包名最好等于目录名。
如果别的包里定义里大写字母开头的结构体类型T,其他包导入这个后,可以使用T,但是无法操作T的小写字母开头的字段(初始化也不行)。
包加载:
常用内置包:
fmt/io/bufio/sort/strconv/os/sync
我们创建的自定义的包,需要放到GOPATH的src目录下。最早的时候,Go语言所依赖的所有的第三方库都放在 GOPATH 这个目录下面,这就导致了同一个库只能保存一个版本的代码。
go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。
export GO111MODULE=on,然后 go mod init会在当前目录生成go.mod文件,以后我们的自定义包就可以放到当前目录下(而不需要放到GOPATH的src下)了。go mod tidy 可以自动下载所需要的包。
同一个包里的文件共享小写开头的变量或类型,包括结构体里小写的字段也是可以直接访问的。夸包就不行。
6.7 单元测试
引用
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=536#/content