本文参考 嗨客网 golang 教程
很多人将 Go 语言 称为 21 世纪的 C 语言,因为 Go 不仅拥有 C 语言的简洁和性能,而且还很好的提供了 21 世纪互联网环境下服务端开发的各种实用特性,让开发者在语言级别就可以方便的得到自己想要的东西。
在 Go 语言的版本迭代过程中,语言特性基本上没有太大的变化,基本上维持在 Go1.1 的基准上,并且官方承诺,新版本对老版本下开发的代码完全兼容。事实上,Go 开发团队在新增语言特性上显得非常谨慎,而在稳定性、编译速度、执行效率以及 GC 性能等方面进行了持续不断的优化。
如果你了解任何一种编程语言,那么通常在学习几个小时就能够掌握 Go 的大部分语法,并在几天后写出你的第一个真正的程序。阅读并理解实效 Go 编程,浏览一下包文档,玩一玩 Gorilla 或者 Go Kit 这样的网络工具包,然后你将成为一个相当不错的 Go 开发者。
这是因为 Go 的首要目标是简单。当我开始学习 Go,它让我想起我第一次 发现 Java:一个简单的语言和一个丰富但不臃肿的标准库。对比当前 Java 沉重的环境,学习 Go 是一个耳目一新的体验。因为 Go 的简易性,Go 程序可读性非常高,虽然错误处理添加了一些麻烦。
Goroutines 可能是 Go 的最佳特性了。它们是轻量级的计算线程,与操作系统线程截然不同。当 Go 程序执行看似阻塞 I/O 的操作时,实际上 Go 运行时挂起了 goroutine ,当一个事件指示某个结果可用时恢复它。与此同时,其他的 goroutines 已被安排执行。因此在同步编程模型下,我们具有了异步编程的可伸缩性优势。
Goroutines 也是轻量级的:它们的堆栈随需求增长和收缩,这意味着有 100 个甚至 1000 个 goroutines 都不是问题。
channel 是 goroutines 的通信方式:它们提供了一个便利的编程模型,可以在 goroutines 之间发送和接收数据,而不必依赖脆弱的低级别同步基本体。channels 有它们自己的一套用法模式。但是,channels 必须仔细考虑,因为错误大小的 channels (默认情况下没有缓冲) 会导致死锁。下面我们还将看到,使用通道并不能阻止竞争情况,因为它缺乏不可变性。
Go 的标准库非常丰富,特别是对于所有与网络协议或 API 开发相关的: http 客户端和服务器,加密,档案格式,压缩,发送电子邮件等等。甚至还有一个 html 解析器和相当强大的模板引擎去生成 text & html,它会自动过滤 XSS 攻击(例如在 Hugo 中的使用)。
各种 APIs 一般都简单易懂。它们有时看起来过于简单:这个某种程度上是因为 goroutine 编程模型意味着我们只需要关心 “看似同步” 的操作。这也是因为一些通用的函数也可以替换许多专门的函数,就像 我最近发现的关于时间计算的问题。
Go 编译为本地可执行文件。许多 Go 的用户来自 Python、Ruby 或 Node.js。对他们来说,这是一种令人兴奋的体验,因为他们看到服务器可以处理的并发请求数量大幅增加。当您使用非并发(Node.js)或全局解释器锁定的解释型语言时,这实际上是相当正常的。结合语言的简易性,这解释了 Go 令人兴奋的原因。
然而与 Java 相比,在原始性能基准测试中,情况并不是那么清晰。Go 打败 Java 地方是内存使用和垃圾回收。Go 的垃圾回收器的设计目的是优先考虑延迟,并避免停机,这在服务器中尤其重要。这可能会带来更高的 CPU 成本,但是在水平可伸缩的体系结构中,这很容易通过添加更多的机器来解决。请记住,Go 是由谷歌设计的,他们从不会在资源上面短缺。
与 Java 相比,Go 的垃圾回收器(GC)需要做的更少:切片是一个连续的数组结构,而不是像 Java 那样的指针数组。类似地,Go maps 也使用小数组作为 buckets,以实现相同的目的。这意味着垃圾回收器的工作量减少,并且 CPU 缓存本地化也更好。
Go 在其标准库中提供了一个很好的测试框架。它支持并行测试、基准测试,并包含许多实用程序,可以轻松测试网络客户端和服务器。
在少既是多中,Rob Pike 解释说 Go 是为了在谷歌取代 C 和 C++,它的前身是 Newsqueak ,这是他在 80 年代写的一种语言。Go 也有很多关于 Plan9 的参考,Plan9 是一个分布式操作系统,在贝尔实验室的 80 年代开发的。
甚至有一个直接从 Plan9 获得灵感的 Go 汇编。为什么不使用 LLVM 来提供目标范围广泛且开箱即用的体系结构?我此处可能也遗漏了某些东西,但是为什么需要汇编?如果你需要编写汇编以充分利用 CPU ,那么不应该直接使用目标 CPU 汇编语言吗?
Go 的创造者应该得到尊重,但是看起来 Go 的设计发生在平行宇宙(或者他们的 Plan9 lab?)中发生的,这些编译器和编程语言的设计在 90 年代和 2000 年中从未发生过。也可能 Go 是由一个会写编译器的系统程序员设计的。
函数式编程吗?不要提它。泛型?你不需要,看看他们用 C++ 编写的烂摊子!尽管 slice、map 和 channel 都是泛型类型,我们将在下面看到。
Go 的目标是替换 C 和 C++,很明显它的创建者也没有关注其他地方。但他们没有达到目标,因为在谷歌的 C 和 C++ 开发人员没有采用它。我的猜测是主要原因是垃圾回收器。低级别 C 开发人员强烈拒绝托管内存,因为他们无法控制什么时间发生什么情况。他们喜欢这种控制,即使它带来了额外的复杂性,并且打开了内存泄漏和缓冲溢出的大门。有趣的是,Rust 在没有 GC 的情况下采用了完全不同的自动内存管理方法。
Go 反而在操作工具的领域吸引了 Python 和 Ruby 等脚本语言的用户。他们在 Go 中找到了一种方法,可以提高性能,减少 内存/cpu/磁盘 占用。还有更多的静态类型,这对他们来说是全新的。Go 的杀手级应用是 Docker ,它在 devops 世界中引起了广泛的应用。Kubernetes 的崛起加强了这一趋势。
Go 接口 就像 Java 接口或 Scala 和 Rust 特性(traits):它们定义了后来由类型实现的行为(我不称之为“类”)。与 Java 接口和 Scala 和 Rust 特性不同,类型不需要显式地指定接口实现:它只需要实现接口中定义的所有函数。所以 Go 的接口实际上是结构化的。
我们可能认为,这是为了允许其他包中的接口实现,而不是它们适用的类型,比如 Scala 或 Kotlin 中的类扩展,或 Rust 特性,但事实并非如此:所有与类型相关的方法都必须在类型的包中定义。
Go 没有枚举,在我看来,这是一个错失的机会。iota 可以快速生成自动递增的值,但它看起来更像一个技巧 而不是一个特性。实际上,由于在一系列的 iota 生成的常量中插入一行会改变下列值的值,这是很危险的。由于生成的值是在整个代码中使用的值,因此这会导致有趣的(而不是!)意外。
这也意味着没有办法让编译器彻底检查 switch 语句,也无法描述类型中允许的值。
很难想象一种没有泛型的现代静态类型化语言,但这就是你在 Go 中看到的:它没有泛型…或者更精确地说,几乎没有泛型,我们会看到它比没有泛型更糟糕。
内置的 slice、map、array 和 channel 都是泛型。声明一个 map[string]MyStruct 清楚地显示了具有两个参数的泛型类型的使用。这很好,因为它允许类型安全编程捕获各种错误。
然而,没有用户可定义的泛型数据结构。这意味着您不能定义可重用的抽象,它可以以类型安全的方式使用任何类型。您必须使用非类型 interface{},并将值转换为适当的类型。任何错误只会在运行时被抓住,会导致 panic。对于 Java 开发人员来说,这就像回到 回退 Java 5 个版本到 2004 年。
普通赋值:
// var 变量名称 变量类型 = 值
var num int = 1
平行赋值:
var num1,num2 int = 1, 2
多行赋值:
var (
num1 int = 1
num2 int = 2
)
Go 的 整数类型 一共有 10 个其中计算架构相关的整数类型有两个,即有符号的整数类型 int 和无符号的整数类型 uint。在不同计算架构的计算机上,它们体现的宽度(存储某个类型的值所需要的空间)是不一样的。空间的单位可以是 bit 也可以是字节 byte。
除了这两个计算架构相关的整数类型之外,还有 8 个可以显式表达自身宽度的整数类型:
如果以 8 进制为变量 num 赋值:
num = 039 // 用"0"作为前缀以表明这是8进制表示法
如果以 16 进制为变量 num 赋值:
num = 0x39
浮点数 类型有两个:float32/float64。浮点数类型的值一般由整数部分、小数点 “.” 和小数部分组成。另外一种表示方法是在其中加入指数部分。指数部分由 “E” 或 “e” 以及带正负号的 10 进制整数表示。
复数类型有两个:complex64 和 complex128。实际上,complex64 类型的值会由两个 float32 类型的值分别表示复数的实数部分和虚数部分。而 complex128 类型的值会由两个 float64 类型的值表示复数的实数部分和虚数部分。
byte 与 rune 都属于别名类型。byte 是 uint8 的别名类型,而 rune 是 int32 的别名类型。一个 rune 的类型值即可表示一个 Unicode 字符。一个 Unicode 代码点通常由 “U+” 和一个以十六进制表示法表示的整数表示。
字符串 的表示法有两种,即:原生表示法和解释型表示法。原生表示法,需用用反引号 “`” 把字符序列包起来,如果用解释型表示法,则需要用双引号 “”" 包裹字符序列。
var str1 string = “str”
var str1 string = `str`
二者的区别是,前者表示的是所见即所得的(除了回车符)。后者所表示的值中转义符会起作用。字符串值是不可变的,如果我们创建了一个此类型的值,就不可能再对它本身做任何修改。
一个数组是可以容纳若干相同类型的元素的容器。数组的长度是固定的。如下声明一个数组类型:
type MyNumbers [3]int
类型声明语句由关键字 type、类型名称和类型字面量组成,上面这条类型声明语句实际上是为数组类型 [3]int 声明了一个别名类型。这使得我们可以把 MyNumbers 当作数组类型 [3]int 来使用。
我们表示这样一个数组类型的值的时候。应该把该类型的类型字面量写在最左边,然后用花括号包裹该值包含的若干元素,各元素之间以(英文半角)逗号分割,即:
[3]int{1,2,3}
现在我们把这个数组字面量赋给一个名为 numbers 的变量:
var numbers = [3]int{1,2,3}
这是一条变量声明语句,它在声明变量的同时为该变量赋值,另一种方式是在其中的类型字面量中省略代表其长度的数组,例:
var numbers = [...]int{1,2,3}
可以用如下方式访问该变量中的任何一个元素。例:
numbers[0]
numbers[1]
numbers[2]
如果要修改数组值中的某一个元素值,可以:
numbers[1] = 4
可以用如下方式获取数组长度:
var length = len(numbers)
如果一个数组没有赋值,则它的默认值为
[length]type{0,0,0…}
切片(slice)与数组一样也是可以若干相同类型元素的容器。与数组不同的是切片类型的长度不确定。每个切片值都会将数组作为其底层数据结构。表示切片类型的字面量如:
[]int
或者是:
[]string
切片类型的声明可以这样:
type MySlice []int
对切片值的表示也与数组值相似
[]int{1,2,3}
操作数组值的方法同样适用于切片值。还有一种操作数组的方式叫做“切片”,实施切片操作的方式就是切片表达式。例:
var number3 = [5]int{1,2,3,4,5}
var slice1 = numbers3[1:4]
上例中切片表达式 numbers3[1:4] 的结果为 []int{2,3,4} 很明显被切下的部分不包含元素上界索引指向的元素。实际上 slice1 这个切片值的底层数组正是 number3 的值。我们也可以在切片值上实施切片操作:
var slice2 = slice1[1:3]
除了长度切片值以及数组值还有另外一个属性–容量。数组的容量总是等于其长度,而切片值的容量往往与其长度不同。如下图:
如图所示,一个切片值的容量即为它的第一个元素值在其底层数组中的索引值与该数组长度的差值的绝对值。可以使用cap()内建函数获取数组、切片、通道类型的值的容量:
var capacity2 int = cap(slice2)
切片类型属于引用类型,它的零值即为 nil,即空值。如果我们只声明了一个切片类型而不为它赋值,则它的默认值 nil。切片的更多操作方法有些时候我们可以在方括号中放入第三个正整数。
numbers3[1:4:4]
第三个正整数为容量上界索引,它意义在于可以把作为结果的切片值的容量设置的更小。它可以限制我们通过这个切片值对其底层数组中的更多元素的访问。上节中 numbers3 和 slice 的赋值语句如下:
var numbers3 = [5]int{1,2,3,4,5}
var slice1 = numbers3[1:4]
这时,变量 slice1 的值是 []int{2,3,4}。但是我们可以通过如下操作将其长度延展与其容量相同:
slice1 = slice1[:cap(slice1)]
通过此操作,变量 slice1 的值变为了 []int{2,3,4,5},且其长度和容量均为 4。现在 number3 的值中的索引值在 (1,5) 范围内的元素都被体现在了 slice1 的值中。这是以 number3 的值是 slice1 的值的底层数组为前提的。
这意味着我们可以轻而易举地通过切片访问其底层数组中对应索引值更大的更多元素。如果我们编写的函数返回了这样一个切片值,那么得到它的程序很可能会通过这种技巧访问到本不应该暴露给它的元素。如果我们在切片中加入了第三个索引(即容量上限索引),如:
var slice1 = numbers3[1:4:4]
那么在此之后,我们将无法通过 slice1 访问到 number3 的值中的第五个元素。虽然切片值在上述方面受到了其容量的限制。但是我们可以通过另外一种手段对其进行不受限制的扩展。这需要用到内建函数 append。append 会对切片值进行扩展并返回一个新的切片值,使用方法如下:
slice1 = append(slice1, 6, 7)
通过上述操作,slice1 的值变为了 []int{2,3,4,6,7}。一旦扩展操作超出了被操作的切片值的容量,那么该切片的底层数组就会被替换。最后一种操作切片的方式是 “复制”。该操作的实施方法是调用 copy 函数。
该函数接收两个类型相同的切片值作为参数,并把第二个参数值中的元素复制到第一个参数值中的相应位置(索引值相同)上。这里有两点需要注意:这种复制遵循最小复制原则,即:被复制的元素的个数总是等于长度较短的那个参值的长度。
与 append 函数不同,copy 函数会直接对其第一个参数值进行修改。
var slice4 = []int{0,0,0,0,0,0}
copy(slice4, slice1)
通过上述复制操作,slice4 会变成 []int{2,3,4,6,7,0,0}。
Go 语言的字典(Map)类型是一个哈希表的实现。字典类型的字面量如下:
map[K]T
其中,“K” 为键的类型,而 “T” 则代表元素(值)的类型。如果我们描述一个键类型为 int,值类型为 string 的字典类型的话:
map[int]string
字典的键类型必须是可比较的,否则会引起错误,即键不能是切片、字典、函数类型。
字典值的字面量表示法实际上与数组的切片的字面量表示法很相似。最左边仍然是类型字面量,右边紧挨着由花括号包裹且有英文逗号分隔的键值对。每个键值对的键和值之间由冒号分隔。以字典类型 map[int]string 为例。他的值的字面量可以是这样的:
map[int]string{1:"a",2:"b"m,3:"c"}
我们可以把这个值赋给一个变量:
mm := map[int]string{1:"a",2:"b",3:"c"}
可用索引表达式取出字典中的值:
b := mm[2]
可以用索引表达式赋值:
mm[2] = b + "2"
这样 mm 中键为 2 的值变为了 “b2”。可以用如下方式向字典中添加一个键值对:
mm[4] = ""
对于字典值来说,如果指定键没有对应的值则默认为该类型的空值。所以 mm[5] 会返回一个 “”。但是这样的话我们就不知道 mm[5] 到底是 “” 还是 mm[5] 没有这个值。所以 go 提供了另外一种写法:
e, ok := mm[5]
针对字典的索引表达式可以有两个求职结果,第二个求职结果是 bool 类型的。它用于表明字典值中是否存在指定的键值对。 从字典中删除键值对的方法非常简单,仅仅是调用内建函数 delete:
delete(mm, 4)
无论 mm 中是否存在以 4 为键的键值对,delete 都删除。 字典类型属于引用类型,它的零值即为 nil。
通道(Channel)是 Go 语言中一种非常独特的数据结构。它可用于在不同 Goroutine 之间传递类型化的数据。并且是并发安全的。相比之下,之前几种数据类型都不是并发安全的。
Goroutine 可以被看作是承载可被并发执行的代码块的载体。它们由 Go 语言的运行时系统调度,并依托操作系统线程(又称内核线程)来并发地执行其中的代码块。
通道类型的表示方法很简单,仅由两部分组成:
chan T
在这个类型字面量中,左边是代表通道类型的关键字 chan,而右边则是一个可变的部分,即代表该通道类型允许传递的数据的类型(或称通道的元素类型)。
与其他的数据类型不同,我们无法表示一个通道类型的值,因此,我们无法用字面量来为通道类型的变量赋值。只能通过调用内建函数 make 来达到目的。make 参数可接受两个参数,第一个参数是代表了将被初始化的值的类型的字面量(例: chan int),而第二个参数则是值的长度,例如,若我们想要初始化一个长度为 5 且元素类型为int的通道值,则需要这样写:
make(chan int, 5)
make 函数也可以被用来初始化切片类型或字典类型的值。暂存在通道值中的数据是先进先出。下面,我们声明一个通道类型的变量,并为其赋值:
ch1 := make(chan string, 5)
这样一来,我们就可以使用接受操作符 <- 向通道值发送数据了。当然,也可以使用它从通道值接收数据,例如,如果我们要向通道 ch1 发送字符串 “value1”,那么应该这样做:
ch1 <- “value1"
如果我们从 ch1 那里接收字符串,则要这样:
<- ch1
我们可以把接受到字符串赋给一个变量,如:
value := <- ch1
与针对字典值的索引表达式一样,针对通道值的接受操作也可以有第二个结果值:
value, ok := <- ch1
这里的 ok 的值是 bool 类型的。它代表了通道值的状态,true 代表通道值有效,而 false 则代表通道值已无效(或称已关闭),更深层次的原因是,如果在接受操作进行之前或过程中通道值被关闭了,则接收操作会立即结束并返回一个该通道值的元素类型的零值。
可以通过函数 close 来关闭通道:
close(ch1)
对通道值的重复关闭会引发运行时异常,会使程序崩溃。在通道值有效的前提下,针对它的发送操作会在通道值已满(其中缓存的数据的个数已等于它的长度)时被阻塞。而向一个已被关闭的通道值发送数据会引发运行时异常。针对有效通道值的接收操作会在它已经为空时被阻塞。通道类型属于引用类型,它的零值为 nil。
对应的关键字为 if、 else 和 else if:
if a := 1; a >= 1 {
fmt.Println("OK")
}
对应的关键字为 switch、 case 和 select:
switch i {
case 0:
fmt.Printf("0")
case 1:
fmt.Printf("1")
case 2:
fallthrough
case 3:
fmt.Printf("3")
case 4, 5, 6:
fmt.Printf("4, 5, 6")
default:
fmt.Printf("Default")
}
对应的关键字为 for 和 range:
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
func myfunc() {
i := 0
HERE:
fmt.Println(i)
i++
if i < 10 {
goto HERE
}
}
首先函数的格式是固定的,func+函数名+ 参数 + 返回值(可选) + 函数体。例 :
func main() {
fmt.Println("Hello go")
}
在 golang 中有两个特殊的 函数,main 函数和 init 函数,main 函数不用介绍在所有语言中都一样,它作为一个程序的入口,只能有一个。init 函数在每个 package 是可选的,可有可无,甚至可以有多个(但是强烈建议一个 package 中一个 init 函数),init 函数在你导入该 package 时程序会自动调用 init 函数,所以 init 函数不用我们手动调用,另外它只会被调用一次,因为当一个 package 被多次引用时,它只会被导入一次。
普通变量
使用普通变量作为函数参数的时候,在传递参数时只是对变量值得拷贝,即将实参的值复制给变参,当函数对变参进行处理时,并不会影响原来实参的值。
指针
函数的变量不仅可以使用普通变量,还可以使用指针变量,使用指针变量作为函数的参数时,在进行参数传递时将是一个地址看呗,即将实参的内存地址复制给变参,这时对变参的修改也将会影响到实参的值。
数组
和其他语言不同的是,go语言在将数组名作为函数参数的时候,参数传递即是对数组的复制。在形参中对数组元素的修改都不会影响到数组元素原来的值。
slice, map, chan
在使用 slice, map, chan 作为函数参数时,进行参数传递将是一个地址拷贝,即将底层数组的内存地址复制给参数 slice, map, chan 。这时,对 slice, map, chan 元素的操作就是对底层数组元素的操作。
函数名字
在 go 语言中,函数也作为一种数据类型,所以函数也可以作为函数的参数来使用。
go 语言可以返回局部变量的指针,因为 go 语言的回收机制是当销毁栈上的临时数据且发现有被外部引用的栈上变量时,会自动转移到堆上。
和其他语言类似,golang 也支持闭包函数:
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
很多人将 Go 语言称为 21 世纪的 C 语言,因为 Go 不仅拥有 C 语言的简洁和性能,而且还很好的提供了 21 世纪互联网环境下服务端开发的各种实用特性,让开发者在语言级别就可以方便的得到自己想要的东西。