https://www.cntofu.com/book/14/eBook/01.2.md
Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。
Go 语言的另一个目标是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机,这一点对于谷歌内部的使用来说就非常重要了。设计者通过 goroutine 这种轻量级线程的概念来实现这个目标,然后通过 channel 来实现各个 goroutine 之间的通信。他们实现了分段栈增长和 goroutine 在线程基础上多路复用技术的自动化。
Go 语言中另一个非常重要的特性就是它的构建速度(编译和链接到机器代码的速度),一般情况下构建一个程序的时间只需要数百毫秒到几秒。
这正是 Go 语言采用包模型的根本原因,这个模型通过严格的依赖关系检查机制来加快程序构建的速度,提供了非常好的可量测性。
而同样作为静态语言的 Go 语言,通过自身优良的构建机制,成功地去除了这个弊端,使得程序的构建过程变得微不足道,拥有了像脚本语言和动态语言那样的高效开发的能力。
Go 语言的设计者们认为内存管理不应该是开发人员所需要考虑的问题。因此尽管 Go 语言像其它静态语言一样执行本地代码,但它依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收(使用了一个简单的标记-清除算法)。
Go 语言还能够在运行时进行反射相关的操作。
Go 语言还支持调用由 C 语言编写的海量库文件
Go 语言没有类和继承的概念, 它通过接口(interface)的概念来实现多态性。Go 语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说这是一门混合型的语言
使用面向对象编程技术显得非常臃肿
Go 语言支持交叉编译,比如说你可以在运行 Linux 系统的计算机上开发运行下 Windows 下运行的应用程序。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
值得注意的是,因为垃圾回收和自动内存分配的原因,Go 语言不适合用来开发对实时性要求很高的软件。
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量 在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递
当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)。
如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
当我们不希望给函数起名字的时候,可以使用匿名函数. 这样的一个函数不能够独立存在 但可以被赋值于某个变量,即保存函数的地址到变量中
匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态 另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁
v := make([]int, 10, 50) 这样分配一个有 50 个 int 值的数组,并且创建了一个长度为 10,容量为 50 的 切片 v,该 切片 指向数组的前 10 个元素。
结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字 .在一个结构体中对于每一种数据类型只能有一个匿名字段。
当系统调用(比如等待 I/O)阻塞协程时,其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。
协程是轻量的,比线程更轻。它们痕迹非常不明显(使用少量的内存和资源):使用 4K 的栈内存就可以在堆中创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个地址空间中 100,000 个连续的协程)。并且它们对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。
在其他语言中,比如 C#,Lua 或者 Python 都有协程的概念。这个名字表明它和 Go协程有些相似,不过有两点不同:
Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的 Go 协程通过通道来通信;协程通过让出和恢复操作来通信 Go 协程比协程更强大,也很容易从协程的逻辑复用到 Go 协程。
不论它启动了多少个协程;所以这些协程是并发运行的,但他们不是并行运行的:同一时间只有一个协程会处在运行状态。 ,为了使你的程序可以使用多个核心运行,这时协程就真正的是并行运行了,你必须使用 GOMAXPROCS 变量。这会告诉运行时有多少个协程同时执行。
main() 等待了 1 秒让两个协程完成,如果不这样,sendData() 就没有机会输出
协程通过在通道 ch 中放置一个值来处理结束的信号。main 协程等待 <-ch 直到从中获取到值。
使用锁的情景: 访问共享数据结构中的缓存信息 保存应用程序上下文和状态信息数据
使用通道的情景: 与异步操作的结果进行交互 分发任务 传递数据所有权 当你发现你的锁使用规则变得很复杂时,可以反省使用通道会不会使问题变得简单些。
x int, y int 被缩写为
x, y int
没有参数的 return 语句返回已命名的返回值。也就是 直接 返回。https://tour.go-zh.org/basics/7
var c, python, java = true, false, "no!"
表达式 T(v) 将值 v 转换为类型 T。
常量的声明与变量类似,只不过是使用 const 关键字。 常量可以是字符、字符串、布尔值或数值。 常量不能用 := 语法声明。
此时你可以去掉分号,因为 C 的 while 在 Go 中叫做 for。
同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。
switch i {
case 0:
case f():
}
type Vertex struct {
X int
Y int
}
Vertex{1, 2}
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。
类型 [n]T
表示拥有 n 个 T 类型的值的数组。var a [10]int
,数组不能改变大小。
[]bool{true, true, false}
类型 []T 表示一个元素类型为 T 的切片。
a[1:4] 创建了一个切片,它包含 a 中下标从 1 到 3 的元素
切片并不存储任何数据,它只是描述了底层数组中的一段。 更改切片的元素会修改其底层数组中对应的元素。 与它共享底层数组的切片都会观测到这些修改
切片拥有 长度 和 容量。 切片的长度就是它所包含的元素个数。切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。 切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。 https://tour.go-zh.org/moretypes/11
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:
a := make([]int, 5) // len(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
[:2] 不会改变容量,只改变长度 [2:] 舍弃前两个值,改变长度、容量?
当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。 若你只需要索引,忽略第二个变量即可。 for i := range pow
若顶级类型只是一个类型名,你可以在文法的元素中省略它。
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
delete(m, key)
elem, ok = m[key] // 0, false
同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。
函数的闭包 func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) }
func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } 返回一个函数,输入,输出都是int类型。函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。 https://tour.go-zh.org/moretypes/25
Go 没有类。不过你可以为结构体类型定义方法
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
指针接收者, 方法必须用指针接受者来更改 main 函数中声明的 Vertex 的值https://tour.go-zh.org/methods/4 !!结构体传参要传指针,不然无法修改原值,传递的是副本:接收的方法的类型是指针!!
接受一个值作为参数的函数必须接受一个指定类型的值
而以值为接收者的方法被调用时,接收者既能为值又能为指针p.Abs() 会被解释为 (*p).Abs() https://tour.go-zh.org/methods/7
接口类型 是由一组方法签名定义的集合。 接口类型的变量可以保存任何实现了这些方法的值。
type Abser interface {
Abs() float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
*Vertex 实现了 Abser 但是 v 是一个 Vertex(而不是 *Vertex)则没有实现Abser
类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。
接受一个值作为参数的函数必须接受一个指定类型的值: 而以值为接收者的方法被调用时,接收者既能为值又能为指针:
1.1 参数的使用
形式参数:定义函数时,用于接收外部传入的数据,叫做形式参数,简称形参。
实际参数:调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。
describe(i) 接口值可以看做包含值和具体类型的元组:(value, type) 接口值既不保存值也不保存具体类型。(, )
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
var i interface{} = "hello"
f, ok := i.(float64) 类似类型转换?
switch v := i.(type) {
case T:
type Stringer interface {
String() string
}
=> __str__
Go 程(goroutine)是由 Go 运行时管理的轻量级线程。go 启动一个新的环境去执行,不阻塞
https://tour.go-zh.org/concurrency/2 ch <- v // 将 v 发送至信道 ch。 v := <-ch // 从 ch 接收值并赋予 v
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
go sum(s[:len(s)/2], c) go sum(s[:len(s)/2], c) x, y := <-c, <-c // 从 c 中接收 等sum都完成后,一起返回<-c,才能走下一步 仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。
可以理解ch就是一个消息队列,消费者生产者?go执行的函数,两个是可以当成是平行执行的
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。 当 select 中的其它分支都没有准备好时,default 分支就会执行。
select 感觉就是一个业务逻辑集合,可以获取到各个ch的返回,并进行处理, 其中要有终止的信号。具体是用来做啥?
,我们通常使用 互斥锁(Mutex)
如果变量或者方法是不能通过对象访问到的,这称作封装的变量或者方法。封装(有时候称作数据隐藏)是面向对象编程中重要的一方面。
Go 语言只有一种方式控制命名的可见性∶定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不导出。同样的机制也同样作用于结构体内的字段和类型中的方法。结论就是,要封装一个对象,必须使用结构体。
另一个结论就是在 Go 语言中封装的单元是包而不是类型。无论是在函数内的代码还是方法内的代码,结构体类型内的字段对于同一个包中的所有代码都是可见的。
确实什么信息也得不到。看起来这个接口没有任何用途,但实际上称为空接口类型的interface{是不可缺少的。正因为空接口类型对其实现类型没有任何要求,所以我们可以把任何值赋给空接口类型。
当然,即使我们创建了一个指向布尔值、浮点数、字符串、map、指针或者其他类型的interface{}接口,也无法直接使用其中的值,毕竟这个接口不包含任何方法。我们需要一个方法从空接口中还原出实际值,在7.10节中我们可以看到如何用类型断言来实现该功能。
接口值可以用==和!=操作符来做比较。如果两个接口值都是 nil或者二者的动态类型完全一致且二者动态值相等(使用动态类型的==操作符来做比较),那么两个接口值相等。因为接口值
需要注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如 slice),那么这个比较会以崩溃的方式失败∶
当处理错误或者调试时,能拿到接口值的动态类型是很有帮助的。可以使用fmt 包的%T 来实现这个需求∶
注意∶含有空指针的非空接口 空的接口值(其中不包含任何信息)与仅仅动态值为 nil的接口值是不一样的。这种微妙的区别成为让每个Go 程序员都困惑过的陷阱。
当main 函数调用f时,它把一个类型为bytes.Buffer 的空指针赋给了out 参数,所以out的动态值确实为空。但它的动态类型是bytes.Buffer,这表示 out是一个包含空指针的非空接口(见图7-5),所以防御性检查 out!= nil仍然是 true。
TODO: 接口与nil空值的区别与联系?
唯一要记住的ᱟ使用 &y 在 return 表返回 y 变量的内存地址。
如您所知,* 符号解引用一个指针变量,就是它返回存储在内存地址里的实际值而不是内存地址本身。
GO 入门指南
内置函数 make 可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片 和映射字面量,或者使用字面量作为变量的初始值。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型 都是默认使用引用传递(即使没有显示的指出指针)。
16.7 使用值类型时误用指针
将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一 直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个 指针,而不是一个值类型,go编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以 会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,我们没有任何收获。
Go 语言 中 sync.Map 的元素遍历,不可以使用 for 循环 或者 for range 循环,而是使用 Range 配合一个回调 函数 进行遍历操作。
通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
假设
T
类型的方法上接收器既有T
类型的,又有*T
指针类型的,那么就不可以在不能寻址的T
值上调用*T
接收器的方法
&B{}
是指针,可寻址B{}
是值,不可寻址b := B{}
b是变量,可寻址
package main
import (
"fmt"
)
func main() {
var i *int
*i=10
fmt.Println(*i)
}
从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,否则我们的值放在哪里去呢?这就是上面错误提示的原因。
对于值类型的声明不需要,是因为已经默认帮我们分配好了。
要分配内存,就引出来今天的 new 和 make。
func main() {
var i *int
i=new(int)
*i=10
fmt.Println(*i)
}
它只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零,也就是类型的零值。
这就是 new,它返回的永远是类型的指针,指向分配类型的内存地址。
make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和 new 是不一样的。
所以从这里可以看的很明白了,二者都是内存的分配(堆上),但是 make 只用于 slice、map 以及 channel 的初始化(非零值);而 new 用于类型的内存分配,并且内存置为零。所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。
make 返回的还是这三个引用类型本身;而 new 返回的是指向类型的指针。
其实 new 不常用,new 这个内置函数,可以给我们分配一块内存让我们使用,但是现实的编码中,它是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的
make 函数是无可替代的,我们在使用 slice、map 以及 channel 的时候,还是要使用 make 进行初始化,然后才才可以对他们进行操作。
Expires : time.Now().Add(10 * time.Second),//正常数字相乘没错
Expire:=12345
Expires : time.Now().Add(Expire * time.Second), //表达式相乘报错
Expire:=12345
Expires : time.Now().Add(time.Duration(Expire) * time.Second), //这样写就对了
使用 var p *int 声明一个指针类型的变量,当一个指针被定义后没有分配到任何变量时,它的值为 nil。
var t *int
// nil
fmt.Println(t)
// panic: runtime error: invalid memory address or nil pointer dereference
fmt.Println(*t)
var p *int
*p = 1 //错误,1是右值,p是左值引用,无法直接初始化
*p = 1的含义可以理解为,将nil的指针地址的值赋予1。可是p的值是nil,但还没有赋值成地址,因此不能把一个子串赋值给一个nil值。
此外,即使不是赋值,对nil的指针通过*读取也会报错,毕竟读取不到任何地址。无效的赋值即野指针
表达式new(type)将创建一个type类型的匿名变量,所做的是为type类型的新值分配并清零一块内存空间,然后将这块内存空间的地址作为结果返回,而这个结果就是指向这个新的type类型值的指针值,返回的指针类型为*type。此时会在堆空间开辟一个内存地址。随应用程序使用始终存在。
常量、映射索引表达式和函数返回的值不可寻址,因此不能对它们使用 & 运算符。因为没有为它们分配特定的内存;它们可以驻留在处理器寄存器中
var p *int
// Constants, map index expressions, and values returned by a function are not addressable, so you cannot use the & operator on them.
//p = &1
根据经验,您可以将 & 视为获取某个现有变量地址的运算符,但有一个例外:您可以创建一个复合文字并使用 & 来检索其地址,例如,&T{}、&map[string ]int64{"a": 1} 或 &[]int{}
`是有效的表达式。
func (this *Plane) Fly2(){
fmt.Println("Fly2......Num:", this.Num)
}
func test(pl *Plane) {
pl.Fly2()
}
为什么nil访问方法可以,访问成员变量就会crash呢?
关于nil的定义:
// nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
指针类型的nil没有分配内存空闲,对于方法,不需要存储空间,而成员变量需要内存空间存放,所以当nil访问成员变量时,由于引用了无效的内存,所以crash.