本节重点:
- Go语言简介
- 学会安装配置 Go 开发环境
Go 也称为 Golang,是由 Google 开发的一种开源、编译和静态类型的编程语言。Go 语言的创造者们包括 Unix 操作系统和 B 语言(C 语言的前身)的创造者、UTF-8 编码的发明者 Ken Thompson,Unix 项目的参与者、UTF-8编码的联合创始人和 Limbo 编程语言(Go 语言的前身)的创造者 Rob Pike,以及著名的 Javascript 引擎V8的创造者 Robert Griesemer。Go 于 2009 年 11 月公开发布。
Go 是一种语法简单的通用编程语言,并由强大的标准库提供支持。Go 闪耀的关键领域之一是创建高度可用和可扩展的 Web 应用程序。Go 还可用于创建命令行应用程序、桌面应用程序甚至移动应用程序。
当有大量其他语言(例如 python、ruby、nodejs …)做同样的工作时,为什么你会选择 Go 作为你的服务器端编程语言。
以下是我在选择 Go 时发现的一些优点。
语法简洁明了,语言没有臃肿的不必要的功能。这使得编写可读和可维护的代码变得容易。
并发是语言的固有部分。因此,编写多线程程序是小菜一碟。这是通过Goroutine和通道实现的,我们将在接下来的教程中讨论。
Go 是一种编译语言。源代码被编译为本机二进制文件。这在解释语言(例如 nodejs 中使用的 JavaScript)中是缺失的。
Go 编译器非常棒,它从一开始就被设计成快速的。
Go 编译器支持静态链接。整个 Go 项目可以静态链接到一个大二进制文件中,并且可以轻松部署在云服务器中,而无需担心依赖关系。
工具在 Go 中值得特别提及。Go 捆绑了强大的工具,可帮助开发人员编写更好的代码。一些常用的工具是,
Go 使用垃圾收集,因此内存管理几乎是自动进行的,开发人员无需担心管理内存。这也有助于轻松编写并发程序。
语言规范非常简单。整个规范适合一个页面,您甚至可以使用它来编写自己的编译器:)
最后但同样重要的是,Go 是一个开源项目。您可以参与并为Go 项目做出贡献。
以下是一些使用 Go 构建的流行产品。
Go 可以安装在 Mac、Windows 和 Linux 三个平台上。您可以从?官方镜像?下载对应平台的二进制文件
从?官方镜像下载 Mac OS 安装程序。双击开始安装。按照提示操作,这将在/usr/local/go
中安装 Golang,并且还将文件夹/usr/local/go/bin
添加到您的 PATH 环境变量中。
从?官方镜像?下载 MSI 安装程序。双击以开始安装并按照提示进行操作。将 Go 安装在位置c:\Go
中,并将目录c:\Go\bin
添加到您的路径环境变量中。
从?官方镜像下载?tar
?文件并将其解压缩到?/usr/local
。
将?/usr/local/go/bin
?添加到?PATH
?环境变量中。这将在 Linux 中安装 Go。 ```
~/.bash_profile
文件:vi ~/.bash_profile
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=$HOME/go
goroot
为go安装目录gopath
?go工作区,即编写代码存放的目录
当我们配置完毕后,可以执行?source ~/.profile
?更新系统环境变量。
go version
正常输出如下:
$ go version
go version go1.18.1 darwin/amd64
因为网络原因,建议配置国内代理。
~/.bash_profile
:vi ~/.bash_profile
export GOPROXY=https://goproxy.cn
source ~/.bash_profile
至此, go开发环境安装成功。
?
安装检查go版本
在线运行?在线代码运行 (gotribe.cn)
新建一个文件夹
打开终端执行 go mod init learngo
。这将创建一个名为go.mod
新建文件?main.go内容
package main
func main() {
println("Hello world")
}
package main
?- 每个 go 文件都必须以package name语句开头。包用于提供代码划分和可重用性。此处使用包名 main。主要功能应始终驻留在主包中。
运行?go run main.go
Go语言的变量声明格式为:
? var?变量名?变量类型
var?name?string
常量的声明和变量声明非常类似,只是把var
换成了const
,常量在定义的时候必须赋值。
程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。
Go语言的变量声明格式为:
????var?变量名?变量类型
变量声明以关键字var
开头,变量类型放在变量的后面,行尾无需分号。 举个例子:
????var?name?string ????var?age?int ????var?isOk?bool
每声明一个变量就需要写var
关键字会比较繁琐,go语言中还支持批量变量声明:
????var?( ????????a?string ????????b?int ????????c?bool ????????d?float32 ????)
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false
。 切片、函数、指针变量的默认为nil
。
当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
????var?变量名?类型?=?表达式
举个例子:
????var?name?string?=?"pprof.cn" ????var?sex?int?=?1
或者一次初始化多个变量
????var?name,?sex?=?"pprof.cn",?1
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
????var?name?=?"pprof.cn" ????var?sex?=?1
短变量声明
在函数内部,可以使用更简略的 := 方式声明并初始化变量。
package?mainimport?( ????"fmt")//?全局变量mvar?m?=?100func?main()?{ ????n?:=?10 ????m?:=?200?//?此处声明局部变量m ????fmt.Println(m,?n)}
匿名变量
在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)
。 匿名变量用一个下划线_表示,例如:
func?foo()?(int,?string)?{ ????return?10,?"Q1mi"}func?main()?{ ????x,?_?:=?foo() ????_,?y?:=?foo() ????fmt.Println("x=",?x) ????fmt.Println("y=",?y)}
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)
注意事项:
????函数外的每个语句都必须以关键字开始(var、const、func等) ????:=不能使用在函数外。 ????_多用于占位,表示忽略值。
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var
换成了const
,常量在定义的时候必须赋值。
????const?pi?=?3.1415 ????const?e?=?2.7182
声明了pi
和e
这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。
多个常量也可以一起声明:
????const?( ????????pi?=?3.1415 ????????e?=?2.7182 ????)
const
同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:
????const?( ????????n1?=?100 ????????n2 ????????n3 ????)
上面示例中,常量n1、n2、n3
的值都是100
。
iota
是go
语言的常量计数器,只能在常量的表达式中使用。iota
在const
关键字出现时将被重置为0
。const
中每新增一行常量声明将使iota
计数一次(iota
可理解为const
语句块中的行索引)。 使用iota
能简化定义,在定义枚举时很有用。
举个例子:
????const?( ????????????n1?=?iota?//0 ????????????n2????????//1 ????????????n3????????//2 ????????????n4????????//3 ????????)
使用_跳过某些值
????const?( ????????????n1?=?iota?//0 ????????????n2????????//1 ????????????_ ????????????n4????????//3 ????????)
iota
声明中间插队
????const?( ????????????n1?=?iota?//0 ????????????n2?=?100??//100 ????????????n3?=?iota?//2 ????????????n4????????//3 ????????) ????const?n5?=?iota?//0
定义数量级 (这里的<<
表示左移操作,1<<10
表示将1
的二进制表示向左移10
位,也就是由1
变成了10000000000
,也就是十进制的1024
。同理2<<2
表示将2
的二进制表示向左移2
位,也就是由10
变成了1000
,也就是十进制的8
。)
????const?( ????????????_??=?iota ????????????KB?=?1?<<?(10?*?iota) ????????????MB?=?1?<<?(10?*?iota) ????????????GB?=?1?<<?(10?*?iota) ????????????TB?=?1?<<?(10?*?iota) ????????????PB?=?1?<<?(10?*?iota) ????????)
多个iota
定义在一行
????const?( ????????????a,?b?=?iota?+?1,?iota?+?2?//1,2 ????????????c,?d??????????????????????//2,3 ????????????e,?f??????????????????????//3,4 ????????)
一般我们用于判断条件, 它的取值范围为?true
,?false
, 声明如下:
// 短类型声明
var isExit := true
// 或
var isExit bool = false
字符串是 Go 中字节的集合。声明如下:
var say = "hello" //单行字符串
var tag = "\"" // 转义符
var say = `hello` //原样输出
var mLine = `line1 //多行输出
line2
line3
`
var str = "hello, 世界"
还有一些可以对字符串执行的操作。我们将在单独的教程中查看这些内容。
数字类型主要分为有符号数和无符号数,有符号数可以用来表示负数,除此之外它们还有位数的区别,不同的位数代表它们实际存储占用空间,以及取值的范围。
var (
a uint8 = 1
b int8 = 1
)
具体范围
uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
float32 the set of all IEEE-754 32-bit floating-point numbers
float64 the set of all IEEE-754 64-bit floating-point numbers
complex64 the set of all complex numbers with float32 real and imaginary parts
complex128 the set of all complex numbers with float64 real and imaginary parts
Go 对显式类型非常严格。没有自动类型提升或转换。让我们通过一个例子来看看这意味着什么。
package main
import (
"fmt"
)
func main() {
i := 55 //int
j := 67.8 //float64
sum := i + j //int + float64 not allowed
fmt.Println(sum)
}
上面的代码在 C 语言中是完全合法的。但是在 go 的情况下,这是行不通的。i 是 int 类型,j 是 float64 类型。我们正在尝试添加 2 个不同类型的数字,这是不允许的。当你运行程序时,你会得到./prog.go:10:11: invalid operation: i + j (mismatched types int and float64)
要修复错误,i和j应该是相同的类型。让我们将j转换为 int。T(v) 是将值 v 转换为类型 T 的语法
package main
import (
"fmt"
)
func main() {
i := 55 //int
j := 67.8 //float64
sum := i + int(j) //j is converted to int
fmt.Println(sum)
}
现在,当您运行上述程序时,您可以看到122
输出。
赋值也是如此。需要显式类型转换才能将一种类型的变量分配给另一种类型。这在以下程序中进行了解释。
package main
import (
"fmt"
)
func main() {
i := 10
var j float64 = float64(i) //this statement will not work without explicit conversion
fmt.Println("j", j)
}
循环语句用于重复执行一段代码。for
是 Go 中唯一可用的循环。Go 没有其他语言(如 C)中存在的 while 或 do while 循环。
for initialisation; condition; post {
}
初始化语句将只执行一次。循环初始化后,将检查条件。如果条件评估为真,则将执行内部循环的主体{ }
,然后执行 post 语句。post 语句将在循环的每次成功迭代后执行。post语句执行后,条件会被重新检查。如果为真,循环将继续执行,否则 for 循环终止。
在 Go 中,初始化、条件和发布这三个组件都是可选的。让我们看一个例子来更好地理解 for 循环。
让我们编写一个程序,使用 for 循环打印从 1 到 10 的所有数字。
package main
import (
"fmt"
)
func main() {
for i := 1; i <= 10; i++ {
fmt.Printf(" %d",i)
}
}
在上面的程序中,i
被初始化为 1。条件语句将检查?if i <= 10
。如果条件为真,则打印?i
?的值,否则终止循环。post 语句在每次迭代结束时将i
?递增 1。一旦i
大于 10,循环终止。
上面的程序将打印1 2 3 4 5 6 7 8 9 10
在 for 循环中声明的变量仅在循环范围内可用。因此i
不能在 for 循环体之外访问。
该break
语句用于在 for 循环完成正常执行之前突然终止 for 循环,并将控件移动到 for 循环之后的代码行。
让我们编写一个程序,使用 break 打印从 1 到 5 的数字。
package main
import (
"fmt"
)
func main() {
for i := 1; i <= 10; i++ {
if i > 5 {
break // 如果 i > 5 就跳出
}
fmt.Printf("%d ", i)
}
fmt.Printf("\nline after for loop")
}
在上面的程序中,每次迭代都会检查 i 的值。如果 i 大于 5 则break
执行并终止循环。然后执行 for 循环之后的 print 语句。上面的程序会输出,
1 2 3 4 5
line after for loop
该continue
语句用于跳过 for 循环的当前迭代。在 continue 语句之后出现在 for 循环中的所有代码都不会在当前迭代中执行。循环将继续进行下一次迭代。
让我们编写一个程序,使用 continue 打印从 1 到 10 的所有奇数。
package main
import (
"fmt"
)
func main() {
for i := 1; i <= 10; i++ {
if i%2 == 0 {
continue
}
fmt.Printf("%d ", i)
}
}
在上面的程序中,该行if i%2 == 0
检查 i 除以 2 的余数是否为 0。如果为零,则数字为偶数,continue
执行语句,控制移至循环的下一次迭代。因此,将不会调用 continue 之后的 print 语句,并且循环继续进行下一次迭代。上述程序的输出是1 3 5 7 9
注意:
break
?会结束所有循环。continue
?会跳过当前循环直接进入下一次循环。if
是一个具有布尔条件的语句,如果该条件的计算结果为真 ,它将执行一段代码true。如果条件评估为 ,它会执行一个备用的 else 块false。在本教程中,我们将了解使用?if
?语句的各种语法和方法。
if
是一个具有布尔条件的语句,如果该条件为真,则执行第一个代码块。如果条件求值为false
,则执行另一个else
块。在本教程中,我们将学习if
语句的各种语法和使用方法。
if
?主要用于条件判断,语法为:
if 条件 {
# 业务代码
}
先看一个简单例子:
package main
import "fmt"
func main() {
age := 7
if age > 6 {
fmt.Println("good")
}
}
在上面的程序中,第8行age > 6
为条件,求age
是否大于 6。如果是,将打印good
文本并且返回程序。
我们可以在条件中使用?&
?或?||
?来进行组合判断:
package main
import "fmt"
func main() {
age := 7
if age > 6 && age <= 12 {
fmt.Println("It's primary school")
}
}
我们还可以使用?if
…else if
…else
?来实现多分支的条件判断:
package main
import "fmt"
func main() {
age := 13
if age > 6 && age <= 12 {
fmt.Println("It's primary school")
} else if age > 12 && age <= 15 {
fmt.Println("It's middle school")
} else {
fmt.Println("It's high school")
}
}
我们已经看到了各种if-else结构,实际上我们也看到了编写相同程序的多种方法。例如,我们已经看到了使用不同的if else
构造来编写程序来检查数字是偶数还是奇数的多种方法。哪一种是Go中惯用的编码方式?在Go的哲学中,最好避免不必要的代码分支和缩进。人们也认为,越早返回越好。提供了下面前一节的程序:
package main
import (
"fmt"
)
func main() {
if num := 10; num % 2 == 0 { //checks if number is even
fmt.Println(num,"is even")
} else {
fmt.Println(num,"is odd")
}
}
在 Go 的哲学中,编写上述程序的惯用方法是避免使用else
语句,并从if
语句返回条件为真
package main
import (
"fmt"
)
func main() {
num := 10;
if num%2 == 0 { //checks if number is even
fmt.Println(num, "is even")
return
}
fmt.Println(num, "is odd")
}
switch 是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较并执行相应的代码块。它可以被认为是替换复杂if else
从句的惯用方式。 例如:
package main
import "fmt"
func main() {
age := 10
switch age {
case 5:
fmt.Println("The age is 5")
case 7:
fmt.Println("The age is 7")
case 10:
fmt.Println("The age is 10")
default:
fmt.Println("The age is unkown")
}
}
注意:在 Go 中?switch
?只要匹配中了就会中止剩余的匹配项,这和?Java
?很大不一样,它需要使用?break
?来主动跳出。
switch
?的?case
?条件可以是多个值,例如:
package main
import "fmt"
func main() {
age := 7
switch age {
case 7, 8, 9, 10, 11, 12:
fmt.Println("It's primary school")
case 13, 14, 15:
fmt.Println("It's middle school")
case 16, 17, 18:
fmt.Println("It's high school")
default:
fmt.Println("The age is unkown")
}
}
注意: 同一个 case 中的多值不能重复。
switch
?还可以使用?if..else
?作为?case
?条件,例如:
package main
import "fmt"
func main() {
age := 7
switch {
case age >= 6 && age <= 12:
fmt.Println("It's primary school")
case age >= 13 && age <= 15:
fmt.Println("It's middle school")
case age >= 16 && age <= 18:
fmt.Println("It's high school")
default:
fmt.Println("The age is unkown")
}
}
小技巧: 使用?switch
?对?interface{}
?进行断言,例如:
package main
import "fmt"
func checkType(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("%v is an in\n", v)
case string:
fmt.Printf("%v is a string\n", v)
default:
fmt.Printf("%v's type is unkown\n", v)
}
}
func main() {
checkType(8)
checkType("hello, world")
}
本节重点:
- 学会使用数组
数组是具有相同?唯一类型?的一组以编号且长度固定的数据项序列。 例如,整数 5、8、9、79、76 的集合形成一个数组
数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的。
一个数组的表示形式为?T[n]
。n
?表示数组中元素的数量,T
?代表每个元素的类型。元素的数量n
也是该类型的一部分(稍后我们将详细讨论这一点)。
可以使用不同的方式来声明数组,让我们一个一个的来看。
package main
import (
"fmt"
)
func main() {
var a [3]int // 定义长度为 3 的 int 类型数组
fmt.Println(a)
}
var a[3]int
?声明了一个长度为 3 的整型数组。数组中的所有元素都被自动赋值为数组类型的零值。 在这种情况下,a
?是一个整型数组,因此?a
?的所有元素都被赋值为?0
,即 int 型的零值。运行上述程序将输出?[0 0 0]
The index of an array starts from 0 and ends at length - 1. Let’s assign some values to the above array.
数组的索引从0
开始到length - 1
结束。让我们为上面的数组赋值。
package main
import (
"fmt"
)
func main() {
var a [3]int
a[0] = 8
a[1] = 18
a[2] = 88
fmt.Println(a)
}
a[0]
?将值分配给数组的第一个元素。该程序将打印[8 18 88]
也可以简写声明相同的数组
package main
import (
"fmt"
)
func main() {
a := [3]int{8, 18, 88} // 简写模式,在定义的同时给出了赋值
fmt.Println(a)
}
你甚至可以忽略声明数组的长度,并用?...
?代替,让编译器为你自动计算长度,这在下面的程序中实现。
package main
import (
"fmt"
)
func main() {
a := [...]int{8, 18, 88} // 也可以不显式定义数组长度,由编译器完成长度计算
fmt.Println(a)
}
Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。
package main
import "fmt"
func main() {
a := [...]string{"中国", "美国", "日本", "法国"}
b := a // 将 a 数组赋值给数组 b
b[1] = "俄罗斯"
fmt.Println("a is ", a)
fmt.Println("b is ", b)
}
在上面程序的第 7 行,a
的副本被赋给b
。在第 8 行中,b
?的第二个元素改为?俄罗斯
。这不会在原始数组?a
?中反映出来。该程序将输出:
a is [中国 美国 日本 法国]
b is [中国 俄罗斯 日本 法国]
for
?循环可用于遍历数组中的元素。
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a); i++ {
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
}
Go 提供了一种更好、更简洁的方法,通过使用?for
?循环的?range
?方法来遍历数组。range
?返回索引和该索引处的值。让我们使用?range
?重写上面的代码。还可以获取数组中所有元素的总和。
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
sum := float64(0)
for i, v := range a {
fmt.Printf("%d the element of a is %.2f\n", i, v)
sum += v
}
fmt.Println("\nsum of all elements of a",sum)
}
上面程序的第 8 行?for i, v := range a
?利用的是 for 循环 range 方式。 它将返回索引和该索引处的值。 我们打印这些值,并计算数组?a
?中所有元素的总和。 程序的输出如下
0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00
sum of all elements of a 256.5
如果你只需要值并希望忽略索引,则可以通过用?_
?空白标识符替换索引来执行。
for _, v := range a {
}
上面的 for 循环忽略索引,同样值也可以被忽略。
到目前为止我们创建的数组都是一维的,Go 语言可以创建多维数组。
package main
import (
"fmt"
)
func printarray(a [3][2]string) {
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
func main() {
a := [3][2]string{
{"lion", "tiger"},
{"cat", "dog"},
{"pigeon", "peacock"},
}
printarray(a)
}
在上面程序的第 17 行,用简略语法声明一个二维字符串数组?a
?。20 行末尾的逗号是必需的。这是因为根据 Go 语言的规则自动插入分号。至于为什么这是必要的,如果你想了解更多,请阅读?https://golang.org/doc/effective_go.html#semicolons。
上述程序的 输出是:
lion tiger
cat dog
pigeon peacock
这就是数组,尽管数组看上去似乎足够灵活,但是它们具有固定长度的限制,不可能增加数组的长度。这就要用到 切片 了。事实上,在 Go 中,切片比传统数组更常见
本节重点:
- 学会使用切片
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型。
实际开发中我们很少使用数组,取而代之的是切片。切片是一个?长度可变的数组
具有 T 类型元素的切片表示为[]T
package main
import (
"fmt"
)
func main() {
a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] //创建一个切片 a[1] to a[3]
fmt.Println(b)
}
使用语法?a[start:end]
?创建一个从 a 数组索引?start
?开始到?end - 1
?结束的切片。因此,在上述程序的第 9 行中,?a[1:4]
?为从索引?1
?到?3
?创建了?a
?数组的一个切片表示。因此, 切片?b
的值为?[77 78 79]
。
索引从0开头,包括左不包括右
让我们看看另一种创建切片的方法。
package main
import (
"fmt"
)
func main() {
c := []int{6, 7, 8}
fmt.Println(c)
}
在上面程序的第 9 行,c:= [] int {6,7,8}
?创建一个有 3 个整型元素的数组,并返回一个存储在?c
中的切片引用。
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
package main
import (
"fmt"
)
func main() {
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)
}
在上面程序的第 9 行,我们根据数组索引?2,3,4
?创建一个切片?dslice
。for 循环将这些索引中的值逐个递增。当重新使用 for 循环打印数组时,可以看到对切片的更改反映到了数组中。该程序的输出为:
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。
让我们写一段代码来更好地理解这点。
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice))
}
在上面的程序中,从?fruitarray
?的索引?1
?和?2
?创建fruitslice
?。 因此,fruitlice
?的长度为?2
。
fruitarray
?的长度是 7。fruiteslice
?是从?fruitarray
?的索引 1 开始创建的。因此,?fruitslice
的容量是从?fruitarray
?索引为 1开始,也就是说从?orange
?开始,该值为?6
。因此,?fruitslice
的容量为?6
。该程序输出length of slice 2 capacity 6
?。
切片可以重置其容量。任何超出这一点将导致程序运行时抛出错误。
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
fruitslice = fruitslice[:cap(fruitslice)]
fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}
在上面程序的第 11 行中,fruitslice
?的容量是重置的。以上程序输出为:
`
length of slice 2 capacity 6
After re-slicing length is 6 and capacity is 6
func make([]T,len,cap)[]T
?通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度。make 函数创建一个数组,并返回引用该数组的切片。
package main
import (
"fmt"
)
func main() {
i := make([]int, 5, 5)
fmt.Println(i)
}
使用 make 创建切片时默认情况下这些值为零。上面程序的输出为?[0 0 0 0 0]
。
数组的长度是固定的,它的长度不能增加。切片是动态的,使用?append
?可以将新元素追加到切片上。append
?函数的定义是?func append(s[]T,x ... T)[]T
。x ... T
?在函数定义中表示该函数接受参数 x 的个数是可变的。这些类型的函数被称为可变参函数。
有一个问题可能会困扰你。如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么,当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。很酷吧:)。下面的程序会让你清晰理解。
package main
import (
"fmt"
)
func main() {
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars))
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars))
}
在上面程序中,cars
的容量最初是 3。在第 10 行,我们给?cars
?添加了一个新的元素,并把?append(cars, "Toyota")
?返回的切片赋值给?cars
。现在?cars
?的容量翻了一番,变成了 6。上面程序的输出为:
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
可以认为切片在内部由结构类型表示。看起来是这样的:
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
切片包含长度、容量和指向数组第零元素的指针。当切片传递给函数时,即使它是按值传递的,指针变量也会引用同一个底层数组。因此,当切片作为参数传递给函数时,函数内部所做的更改在函数外部也可见。让我们编写一个程序来检查一下。
package main
import (
"fmt"
)
func subtactOne(numbers []int) {
for i := range numbers {
numbers[i] -= 2
}
}
func main() {
nos := []int{8, 7, 6}
fmt.Println("slice before function call", nos)
subtactOne(nos)
fmt.Println("slice after function call", nos)
}
上面程序的行号 17 中,调用函数将切片中的每个元素递减 2。在函数调用后打印切片时,这些更改是可见的。如果你还记得,这是不同于数组的,对于函数中一个数组的变化在函数外是不可见的。上面程序的输出:
slice before function call [8 7 6]
slice after function call [6 5 4]
与数组类似,切片可以有多个维度。
package main
import (
"fmt"
)
func main() {
pls := [][]string {
{"C", "C++"},
{"Java"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
程序的输出如下:
C C++
Java
Go Rust
注意:Go 使用 2x 算法来增加数组长度
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3}
a := s
s[0] = 888
s = append(s, 4)
fmt.Println(a, len(a), cap(a))
fmt.Println(s, len(s), cap(s))
}
?[888 2 3] 3 3
?[888 2 3 4] 4 6
本节重点:
- 学会 map 的基本使用
map 一种无序的键值对, 它是数据结构 hash 表的一种实现方式。map工作方式就是:定义键和值,并且可以获取,设置和删除其中的值。
可以通过将键和值的类型make传递给函数来创建映射。以下是创建新 Map的语法。
make(map[type of key]type of value)
package main
import (
"fmt"
)
func main() {
// 使用关键字 map 来声明
lookup := map[string]int{ "goku": 9001, "gohan": 2044, }
// 使用make来声明
cMap := make(map[string]int)
cMap["北京"] = 1
fmt.Println("lookup:",lookup)
fmt.Println("cMap:",cMap)
}
上面程序用两种方式创建了两个 map,运行结果如下:
lookup: map[gohan:2044 goku:9001]
cMap: map[北京:1]
将元素添加到 Map 的语法与数组相同。上面的程序就是两种添加 map 的方式。
键不一定只能是 string 类型。所有可比较的类型,如 boolean,interger,float,complex,string 等,都可以作为键。即使是用户定义的类型(例如结构)也可以是键。
Map的零值是nil
。如果您尝试向 Map添加元素,则会发生nil
运行时报错。因此,必须在添加元素之前初始化 Map。
package main
func main() {
var employeeSalary map[string]int
employeeSalary["steve"] = 12000
}
在上面的程序中,employeeSalary
为nil
。而我们正在尝试向 Map添加一个新键。程序因错误而报错:panic: assignment to entry in nil map
现在我们已经向 Map添加了一些元素,让我们学习如何检索它们。检索 Map元素的语法为map[key]
package main
import (
"fmt"
)
func main() {
cities := map[string]int{
"北京": 100000,
"湖南": 430000,
}
city := "北京"
postCode :=cities[city]
fmt.Println("城市:", city,"邮编:", postCode)
}
上面的程序非常简单。检索城市邮编。程序打印城市: 北京 邮编: 100000
如果一个元素不存在会发生什么?该映射将返回该元素类型的零值。在cities
?map的情况下,如果我们尝试访问不存在的元素,将返回int类型的零值。
package main
import (
"fmt"
)
func main() {
cities := map[string]int{
"北京": 100000,
"湖南": 430000,
}
fmt.Println("城市:", cities["上海"])
}
上述程序的输出是城市: 0
?当我们尝试检索 Map中不存在的键的值时,不会出现运行时错误。
当键不存在时,将返回类型的零值。当我们想要找出键是否真的存在于 Map 中时,要怎么做?
例如,我们想知道?cities
?Map中是否存在某个键。
value, ok := map[key]
以上是找出特定键是否存在于映射中的语法。如果ok
为真,则该键存在且其值存在于变量value
中,否则该键不存在。
package main
import (
"fmt"
)
func main() {
cities := map[string]int{
"北京": 100000,
"湖南": 430000,
}
newEmp := "上海"
value, ok := cities[newEmp]
if ok == true {
fmt.Println("邮编:", value)
return
}
fmt.Println(newEmp, "邮编不存在")
}
在上面的程序中,在第 13 行。ok
将为?false
,因为上海
不存在。因此程序将打印,
上海 邮编不存在
我们可以用for
循环的range
形式用于迭代 Map的所有元素。
package main
import (
"fmt"
)
func main() {
cities := map[string]int{
"北京": 100000,
"湖南": 430000,
}
for key, value := range cities {
fmt.Printf("cities[%s] = %d\n", key, value)
}
}
上述程序输出,
cities[北京] = 100000
cities[湖南] = 430000
值得注意的是,因为 map 是无序的,因此对于程序的每次执行,不能保证使用?for range
?遍历 map 的顺序总是一致的,而且遍历的顺序也不完全与元素添加的顺序一致。可以试下多添加几个城市验证下。
delete(map, key)
?用于删除 map 中的键。delete
?函数没有返回值。
package main
import (
"fmt"
)
func main() {
cities := map[string]int{
"北京": 100000,
"湖南": 430000,
}
fmt.Println("map before deletion", cities)
delete(cities, "北京")
fmt.Println("map after deletion", cities)
}
上面的程序删除以?北京
?为键的元素。程序输出为:
map before deletion map[北京:100000 湖南:430000]
map after deletion map[湖南:430000]
如果我们尝试删除 Map中不存在的键,则不会出现运行时错误。
==
?进行比较。package main
import (
?? ?"fmt"
)
func main() {
? ?cars := []string{"Ferrari", "Honda", "Ford"}
? fmt.Println(cars,len(cars),cap(cars))
? cars =append(cars,"toyota")
? fmt.Println(cars,len(cars),cap(cars))
}
为什么cap变为6?
func main() {
? ? var employeeSalary map[string]int
? ? employeeSalary["steve"] = 12000
? ? fmt.Println(employeeSalary)
}
为什么会报错?
本节重点:
- 学会 sting 的基本使用
字符串在 Go 中值得特别提及,因为与其他语言相比,string
?的实现方式同其他语言略有不同。
字符串是 Go 中的字节切片。可以通过将一组字符括在双引号中来创建字符串" "
。
让我们看个创建string
并打印它的简单示例。
package main
import (
"fmt"
)
func main() {
web := "https:www.gotribe.cn"
fmt.Println(web)
}
上面的程序将打印https:www.gotribe.cn
.
Go 中的字符串是兼容Unicode编码的,并且是UTF-8编码的。
由于字符串是字节切片,因此可以访问字符串的每个字节。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func main() {
web := "https:www.gotribe.cn"
fmt.Printf("String: %s\n", web)
printBytes(web)
}
%s
?是打印字符串的格式说明符。在16行号,打印输入字符串。在上面程序的第9 行中,len(s)
?返回字符串中的字节数,我们使用for
?循环以十六进制格式符打印这些字节。%x
是十六进制的格式说明符。上述程序输出
String: https:www.gotribe.cn
Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
我们稍微修改一下上面的程序来打印字符串的字符。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
fmt.Printf("Characters: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i])
}
}
func main() {
web := "https:www.gotribe.cn"
fmt.Printf("String: %s\n", web)
printBytes(web)
fmt.Printf("\n")
printChars(web)
}
在上面程序的第 17 行,?%c
?格式说明符用于在printChars
方法中打印字符串的字符。程序打印:
String: https:www.gotribe.cn
Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
Characters: h t t p s : w w w . g o t r i b e . c n
?
上面的程序看起来像是访问字符串中单个字符的合法方式,但它有一个严重的错误。让我们找出那个错误是什么。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
fmt.Printf("Characters: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i])
}
}
func main() {
web := "https:www.gotribe.cn"
fmt.Printf("String: %s\n", web)
printBytes(web)
fmt.Printf("\n")
printChars(web)
fmt.Printf("\n\n")
web = "Se?or"
fmt.Printf("String: %s\n", web)
printBytes(web)
fmt.Printf("\n")
printChars(web)
}
上述程序的输出是
String: https:www.gotribe.cn
Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
Characters: h t t p s : w w w . g o t r i b e . c n
String: Se?or
Bytes: 53 65 c3 b1 6f 72
Characters: S e ? ± o r
上面程序的第 33 行,我们尝试将 “Se?or” 中的每个字符打印出来,但是却得到了 “Se?±or”。为什么这个程序在 “https:www.gotribe.cn” 上运行正常,但是不适用于 “Se?or” 呢?因为 ? 的 Unicode 码点是 U+00F1,因而它的 UTF-8 编码占了两个字节:c3
?和?b1
。上面的程序假定每个码点只有一个字节长度,因此会发生错误。在 UTF-8 编码中,一个码点可能会占一个以上的字节。?在这种情况下,我们需要?rune
?来帮助解决问题。
rune
?是 Go 中的内置类型,它是 int32 的别名。Rune 表示 Go 中的 Unicode 代码点。代码点占用多少字节并不重要,它可以用一个符文来表示。让我们修改上面的程序以使用符文打印字符。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
fmt.Printf("Characters: ")
runes := []rune(s)
for i := 0; i < len(runes); i++ {
fmt.Printf("%c ", runes[i])
}
}
func main() {
web := "https:www.gotribe.cn"
fmt.Printf("String: %s\n", web)
printBytes(web)
fmt.Printf("\n")
printChars(web)
fmt.Printf("\n\n")
web = "Se?or"
fmt.Printf("String: %s\n", web)
printBytes(web)
fmt.Printf("\n")
printChars(web)
}
在上面的程序的 第16行 中,字符串被转换为rune切片。然后我们循环它并显示字符。输出结果为:
String: https:www.gotribe.cn
Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
Characters: h t t p s : w w w . g o t r i b e . c n
String: Se?or
Bytes: 53 65 c3 b1 6f 72
Characters: S e ? o r
这时候结果就正确了。
在 Go 中有多种方法可以执行字符串连接。让我们来看看其中的几个。
执行字符串连接的最简单方法是使用+
运算符。
ackage main
import (
"fmt"
)
func main() {
string1 := "Go"
string2 := "is awesome"
result := string1 + " " + string2
fmt.Println(result)
}
?
在上面的程序中,第 10 行。string1
与中间的空格和string2
连接。该程序打印,Go is awesome
连接字符串的第二种方法是使用 fmt 包的Sprintf函数。
该Sprintf
函数根据输入格式说明符格式化字符串并返回结果字符串。让我们用Sprintf
函数重写上面的程序。
package main
import (
"fmt"
)
func main() {
string1 := "Go"
string2 := "is awesome"
result := fmt.Sprintf("%s %s", string1, string2)
fmt.Println(result)
}
?
上面程序的 第10行%s %s
格式是作为Sprintf
参数输入两个字符串,它们之间有一个空格。这将连接两个字符串。结果字符串存储在result
中. 该程序打印,Go is awesome
注意点:
RuneCountInString(s string) (n int)
来获取字符串的长度。一些 Unicode 字符的代码点占据了超过 1 个字节。len
用于找出这些字符串的长度将返回不正确的字符串长度。?练习题
package main
import (
"fmt"
)
func main() {
num := 65
str := string(num)
fmt.Printf("%v, %T\n", str, str)
}
答案?A, string
65是A的编码,%T指代的是输出类型
本节重点:
- 学会函数的基本使用
函数是执行特定任务的代码块。函数接受输入,对输入执行一些计算,然后生成输出。
在 go 中声明函数的语法是:
func name(parameter) (result-list){
//body
}
函数声明以func关键字开头,后跟name
(函数名). 在括号中指定参数,后面为函数返回值result-list
指定参数的语法是,参数名称后跟类型。可以指定任意数量的参数,例如(parameter1 type, parameter2 type)
。而{,}
内的代码为函数的主体内容。
参数和返回类型在函数中是可选的。因此,以下语法也是有效的函数声明。
func name() {
}
package main
import (
"fmt"
)
func plus(a, b int) (res int){
return a + b
}
func main() {
a, b := 90, 6
sumAll := plus(a, b)
fmt.Println("sum", sumAll)
}
上面程序,函数plus
?接受两个 int 类型的值,并返回最终和。输出结果如下:
sum 96
?
不能传参int a,int b,且需要在后面用括号声明返回值的类型?
?
一个函数可以返回多个值。(这点是go特有的吧)
package main
import (
"fmt"
)
func multi() (string, int) {
return "小李", 18
}
func main() {
name, age := multi()
fmt.Println("name:", name, "age:", age)
}
上述程序降会输出:
name: 小李 age: 18
?
可以从函数返回命名值。如果返回值被命名,则可以认为它在函数的第一行被声明为变量。
// 被命名的返回参数的值为该类型的默认零值
// 该例子中 name 默认初始化为空字符串,height 默认初始化为 0
func namedReturnValue()(name string, height int){
name = "xiaoming"
height = 180
return
}
?
package main
import (
"fmt"
)
func sum(nums ...int)int{
fmt.Println("len of nums is : ", len(nums))
res := 0
for _, v := range nums{
res += v
}
return res
}
func main(){
fmt.Println(sum(1))
fmt.Println(sum(1,2))
fmt.Println(sum(1,2,3))
}
?
func main(){
func(name string){
fmt.Println(name)
}("https://www.gotribe.cn")
}
func add(args ...int) int {
sum := 0
for _, arg := range args {
sum += arg
}
return sum
}
?
练习题
?
?
1、函数定义中使用了可变参数语法(args ...int),这意味着我们可以在函数调用时传递任意数量的int类型参数,包括单个int类型参数、多个int类型参数、以及一个int类型的切片。所以我们可以传递一个int类型的切片作为参数。
2、在传递切片时,我们需要使用切片的展开语法(...)来将切片转换为可变参数列表。这是因为函数定义中使用了可变参数语法(args ...int),这意味着我们需要将切片展开为一个个单独的int类型参数。?