本节重点:
- 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 还可用于创建命令行应用程序、桌面应用程序甚至移动应用程序。
Go 的优点
当有大量其他语言(例如 python、ruby、nodejs …)做同样的工作时,为什么你会选择 Go 作为你的服务器端编程语言。
以下是我在选择 Go 时发现的一些优点。
简单的语法
语法简洁明了,语言没有臃肿的不必要的功能。这使得编写可读和可维护的代码变得容易。
易于编写并发程序
并发是语言的固有部分。因此,编写多线程程序是小菜一碟。这是通过Goroutine和通道实现的,我们将在接下来的教程中讨论。
编译语言
Go 是一种编译语言。源代码被编译为本机二进制文件。这在解释语言(例如 nodejs 中使用的 JavaScript)中是缺失的。
快速编译
Go 编译器非常棒,它从一开始就被设计成快速的。
静态链接
Go 编译器支持静态链接。整个 Go 项目可以静态链接到一个大二进制文件中,并且可以轻松部署在云服务器中,而无需担心依赖关系。
Go 工具
工具在 Go 中值得特别提及。Go 捆绑了强大的工具,可帮助开发人员编写更好的代码。一些常用的工具是,
- gofmt - gofmt用于自动格式化 Go 源代码。它使用制表符进行缩进,使用空白进行对齐。
- vet - vet分析 go 源代码并报告可能的可疑代码。vet 报告的所有内容都不是真正的问题,但它能够捕获编译器未报告的错误,例如使用Printf时不正确的格式说明符。
- golint - golint用于识别代码中的样式问题。
垃圾收集
Go 使用垃圾收集,因此内存管理几乎是自动进行的,开发人员无需担心管理内存。这也有助于轻松编写并发程序。
简单的语言规范
语言规范非常简单。整个规范适合一个页面,您甚至可以使用它来编写自己的编译器:)
开源
最后但同样重要的是,Go 是一个开源项目。您可以参与并为Go 项目做出贡献。
使用 Go 构建的热门产品
以下是一些使用 Go 构建的流行产品。
- 谷歌使用 Go 开发了 Kubernetes。
- Docker,世界著名的容器化平台是使用 Go 开发的。
- Dropbox 已将其性能关键组件从 Python 迁移到 Go。
- Infoblox 的下一代网络产品是使用 Go 开发的。
安装
Go 可以安装在 Mac、Windows 和 Linux 三个平台上。您可以从 官方镜像 下载对应平台的二进制文件
Mac OS
从 官方镜像下载 Mac OS 安装程序。双击开始安装。按照提示操作,这将在/usr/local/go
中安装 Golang,并且还将文件夹/usr/local/go/bin
添加到您的 PATH 环境变量中。
Windows
从 官方镜像 下载 MSI 安装程序。双击以开始安装并按照提示进行操作。将 Go 安装在位置c:\Go
中,并将目录c:\Go\bin
添加到您的路径环境变量中。
Linux
从 官方镜像下载 tar
文件并将其解压缩到 /usr/local
。
将 /usr/local/go/bin
添加到 PATH
环境变量中。这将在 Linux 中安装 Go。 ```
Linux下环境配置详细示例
- 编辑
~/.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
设置Go国内模块代理
因为网络原因,建议配置国内代理。
- 编辑
~/.bash_profile
:
vi ~/.bash_profile
- 追加以下内容:
export GOPROXY=https://goproxy.cn
- 使以上配置的环境变量生效:
source ~/.bash_profile
至此, go开发环境安装成功。
安装检查go版本
第一行god代码
本节重点:
- 跑起第一个go程序
- 了解go的编译过程
本教程适用于 Go 版本 1.18 或更高版本。
创建一个 Go 模块
本节我们将要创建一个简单的程序,然后学习如何编译和运行它。
- 让我们创建一个目录,我们要在其中编写我们的 hello world 程序。打开终端并运行以下命令。
mkdir learngo
learngo上面的命令创建一个名为learngo
的目录。
- 创建一个 Go 模块
Go 模块用于跟踪我们应用程序的依赖项及其版本。下一步是创建一个在文件夹中命名的 go learngo模块learngo
。
在learngo/
目录中运行go mod init learngo
。这将创建一个名为go.mod. 该文件的内容如下所示。
module learngo
go 1.18
第一行 module learngo
指定模块名称。`go 1.18 表示此模块中的文件使用 go 版本 1.18
go mod init
是一个 Go 语言的命令,用于初始化一个新的 Go 项目模块。这个命令会在项目的根目录下创建一个名为go.mod
的文件,该文件用于定义项目的依赖关系。当你运行
go mod init
命令时,你需要提供模块的名称作为参数。在你提供的例子中,命令应该是:
go mod init learngo
这条命令会在当前目录下创建一个名为
go.mod
的文件,并将模块名称设置为 "learngo"。这个模块名称通常应该反映你的项目的名称,并且是唯一的,以便于其他开发者能够识别和查找你的项目。一旦你运行了这个命令,你就可以开始添加项目的依赖项了。你可以使用
go get
命令来获取项目的依赖项,例如:
go get github.com/some/dependency
这将会将指定的依赖项添加到你的
go.mod
文件中,并下载它。请注意,为了运行
go mod init
命令,你需要安装 Go 语言环境,并且设置好GOPATH
或GO111MODULE
环境变量。
Hello world
在learngo
目录中新建 main.go
文件,内容如下:
package main
func main() {
println("Hello world")
}
保存文件名为hello.go
。
接下来,打开一个 shell 或者终端提示符,进入到文件保存的目录内。
最后,通过敲入以下命令来运行程序:
go run hello.go
如果一切正常,你将看到屏幕输出:
Hello world
运行解析
go run
命令已包含了编译和运行。它会先在一个临时目录编译程序,然后执行完后清理掉。我们可以在run
后面加--work
参数来查看临时目录。
go run --work hello.go
#会打印出如下信息:
#WORK=/var/folders/kh/ldpdqcr549x9ff5vt56wq4h40000gn/T/go-build446426376
如果要明确编译代码的话,可以通过go build
命令
go build hello.go
使用go build
后,会在目录下生成一个名为hello
的可执行文件。可通过./hello
直接执行。
Hello world程序的简要说明
这是我们刚刚编写的hello world程序
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
我们将简要讨论程序的每一行的作用。在接下来的教程中,我们将深入探讨程序的每个部分。
package main
- 每个 go 文件都必须以package name语句开头。包用于提供代码划分和可重用性。此处使用包名 main。主要功能应始终驻留在主包中。
import "fmt"
- import 语句用于导入其他包。在我们的例子中,fmt包被导入,它将在 main 函数中用于将文本打印到标准输出。
func main()
-func关键字标记函数的开始。这main是一个特殊的功能。程序执行从main函数开始。{和大}括号表示主函数的开始和结束。
fmt.Println("Hello World")
- 该包的Println功能fmt用于将文本写入标准输出。package.Function()是调用包中函数的语法。
运行go
在线运行 在线代码运行 (gotribe.cn)
vscod中运行
运行 go run main.go
go变量和常量
变量
1.1.1. 变量的来历
程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
1.1.2. 变量类型
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
1.1.3. 变量声明
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。
1.1.4. 标准声明
Go语言的变量声明格式为:
var 变量名 变量类型 (C,java,python都是先写)
变量声明以关键字var
开头,变量类型放在变量的后面,行尾无需分号。 举个例子:(这个方式像javascript)
var name string var age int var isOk bool
1.1.5. 批量声明
每声明一个变量就需要写var
关键字会比较繁琐,go语言中还支持批量变量声明:
var ( a string b int c bool d float32 )
1.1.6. 变量的初始化
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等) :=不能使用在函数外。 _多用于占位,表示忽略值。
常量
常量的声明与变量类似,只不过是使用const
关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 语法声明。:=
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把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
。
1.2.1. iota
iota
是go
语言的常量计数器,只能在常量的表达式中使用。iota
在const
关键字出现时将被重置为0
。const
中每新增一行常量声明将使iota
计数一次(iota
可理解为const
语句块中的行索引)。 使用iota
能简化定义,在定义枚举时很有用。
举个例子:
const ( n1 = iota //0 n2 //1 n3 //2 n4 //3 )
1.2.2. 几个常见的iota示例:
使用_跳过某些值
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 )
go基础数据类型
布尔类型
一般我们用于判断条件, 它的取值范围为 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
)
具体范围
uint
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)
int
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
byte //uint8的别名
rune //int32的别名
package main
import (
"fmt"
"math/cmplx"
)
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
零值
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
- 数值类型为 ,
0
- 布尔类型为 ,
false
- 字符串为 (空字符串)。
""
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
fmt.Printf("%v %v %v %q\n", i, f, b, s)
}
类型转换
表达式 T(v)
将值v 转换为类型 T
。v
一些关于数值的转换:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
或者,更加简单的形式:
i := 42
f := float64(i)
u := uint(f)
与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换
package main
import (
"fmt"
"math"
)
func main() {
var x, y int = 3, 4
var f float64 = math.Sqrt(float64(x*x + y*y))
var z uint = uint(f)
fmt.Println(x, y, z)
}
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)
}
类型推导
在声明一个变量而不指定其类型时,变量的类型由右值推导得出。
package main
import "fmt"
func main() {
v := 42 // 修改这里!
fmt.Printf("v is of type %T\n", v)
}
当右值声明了类型时,新变量的类型与其相同:
var i int
j := i // j 也是一个 int
不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 , 或 了,这取决于常量的精度:int
float64
complex128
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
尝试修改示例代码中 的初始值,并观察它是如何影响类型的。v
循环与条件判断
for
循环语句用于重复执行一段代码。for
是 Go 中唯一可用的循环。Go 没有其他语言(如 C)中存在的 while 或 do while 循环。
for initialisation; condition; post {
}
基本的 循环由三部分组成,它们用分号隔开:for
- 初始化语句:在第一次迭代前执行
- 条件表达式:在每次迭代前求值,一旦条件表达式的布尔值为
false
,循环迭代就会终止。 - 后置语句:在每次迭代的结尾执行
初始化语句通常为一句短变量声明,该变量声明仅在 语句的作用域中可见。for
注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 则是必须的。{ }
初始化语句将只执行一次。循环初始化后,将检查条件。如果条件评估为真,则将执行内部循环的主体{ }
,然后执行 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 循环体之外访问。
for 是 Go 中的 “while”
此时你可以去掉分号,因为 C 的 在 Go 中叫做 。while
for
package main
import "fmt"
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
break语句
该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
该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 else 语句
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")
}
}
Go 的使用习惯
我们已经看到了各种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
Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的switch,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 语句。 除非以 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数
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 语句从上到下顺次执行,直到匹配成功时停止。
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")
}
没有条件的 switch
没有条件的 switch 同 switch true
一样。
这种形式能将一长串 if-then-else 写得更加清晰。
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Println(t)
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
九阴真经
数组
本节重点:
- 学会使用数组
数组是具有相同 唯一类型 的一组以编号且长度固定的数据项序列。 例如,整数 5、8、9、79、76 的集合形成一个数组
数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的。数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。
数组的声明
一个数组的表示形式为 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 [中国 俄罗斯 日本 法国]
使用 range 遍历数组
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
使用 make 创建一个切片
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 的基本使用
概念
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 的语法与数组相同。上面的程序就是两种添加 map 的方式。
键不一定只能是 string 类型。所有可比较的类型,如 boolean,interger,float,complex,string 等,都可以作为键。即使是用户定义的类型(例如结构)也可以是键。
Map的零值
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
,因为上海
不存在。因此程序将打印,
上海 邮编不存在
遍历 Map中的所有元素
我们可以用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 的顺序总是一致的,而且遍历的顺序也不完全与元素添加的顺序一致。可以试下多添加几个城市验证下。
从 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中不存在的键,则不会出现运行时错误。
注意
- 与切片一样,maps 是引用类型。当一个 map 赋值给一个新的变量,它们都指向同一个内部数据结构。因此改变其中一个也会反映到另一个。
- 须指定 key, value 的类型,插入的纪录类型必须匹配。
- key 具有唯一性,插入纪录的 key 不能重复。
- KeyType 可以为基础数据类型(例如 bool, 数字类型,字符串), 不能为数组,切片,map,它的取值必须是能够使用
==
进行比较。 - ValueType 可以为任意类型。
- 无序性。
- 线程不安全, 一个 goroutine 在对 map 进行写的时候,另外的 goroutine 不能进行读和写操作,Go 1.6 版本以后会抛出 runtime 错误信息。
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
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
注意点:
- 字符串在 Go 中是不可变的。一旦创建了字符串,就无法更改它。
- utf8包提供了
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)
。而{,}
内的代码为函数的主体内容。
- 函数组成
- 函数名
- 参数列表(parameter)
- 返回值(result-list)
- 函数体(body)
参数和返回类型在函数中是可选的。因此,以下语法也是有效的函数声明。
func name() {
}
函数
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
在本例中,
x int, y int
被缩写为
x, y int
示例函数
函数可以没有参数或接受多个参数
单返回值函数
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
返回值为map
当函数返回值为Map时,你可以按照以下示例进行操作:
package main
import "fmt"
// 函数返回一个字符串类型的Map
func getMap() map[string]string {
myMap := make(map[string]string)
myMap["key1"] = "value1"
myMap["key2"] = "value2"
return myMap
}
func main() {
result := getMap()
fmt.Println(result) // 输出: map[key1:value1 key2:value2]
}
在上面的示例中,我们定义了一个名为getMap的函数,它返回一个字符串类型的Map。在函数内部,我们使用make函数创建了一个空的Map,并添加了两个键值对。最后,我们通过return语句将Map返回给调用者。在main函数中,我们调用getMap函数并将返回的结果存储在变量result中。然后,我们打印出该Map的内容。
多返回值函数
一个函数可以返回多个值。(这点是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
命名返回值
可以从函数返回命名值。如果返回值被命名,则可以认为它在函数的第一行被声明为变量。
Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。
返回值的名称应当具有一定的意义,它可以作为文档使用。
没有参数的 return
语句返回已命名的直接
返回值。也就是 返回。
直接返回语句应当仅用在下面这样的短函数中。在长的函数中它们会影响代码的可读性。
// 被命名的返回参数的值为该类型的默认零值
// 该例子中 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
}
入参为数组
当函数的入参为一个数组时,你可以按照以下方式编写 Go 语言代码:
package main
import "fmt"
// 函数接受一个整数数组作为参数
func processArray(arr []int) {
// 在函数内部,可以通过索引访问数组元素
for i := 0; i < len(arr); i++ {
// 对数组元素进行操作
arr[i] *= 2
}
}
func main() {
// 创建一个整数数组
numbers := []int{1, 2, 3, 4, 5}
// 调用函数并传递数组作为参数
processArray(numbers)
// 打印处理后的数组元素
fmt.Println(numbers) // 输出: [2 4 6 8 10]
}
在上面的示例中,processArray 函数接受一个整数数组 arr 作为参数。在函数内部,我们使用 for 循环遍历数组,并对每个元素进行操作(这里是将其乘以 2)。最后,我们在 main 函数中创建了一个整数数组 numbers,并将其作为参数传递给 processArray 函数。处理后的数组元素通过 fmt.Println 打印出来。
练习题
1、函数定义中使用了可变参数语法(args ...int),这意味着我们可以在函数调用时传递任意数量的int类型参数,包括单个int类型参数、多个int类型参数、以及一个int类型的切片。所以我们可以传递一个int类型的切片作为参数。
2、在传递切片时,我们需要使用切片的展开语法(...)来将切片转换为可变参数列表。这是因为函数定义中使用了可变参数语法(args ...int),这意味着我们需要将切片展开为一个个单独的int类型参数。
指针
本节重点:
- 理解并使用 Go 指针
在本节中,我们将学习指针在 Go 中是如何工作的,我们还将了解 Go 指针与其他语言(如 C 和 C++)中的指针有何不同。
概念
指针是存储另一个变量的内存地址的变量。
![[Pasted image 20220802230857.png]]
在上图中,变量 b
的值是 156
,存储在地址为 0x1040a124
的内存中。变量 a
存储了变量 b
的地址。现在可以说 a
指向 b
。
指针的声明
指向类型 T
的指针用 *T
表示。
让我们编写一个声明指针的程序。
package main
import (
"fmt"
)
func main() {
b := 255
var a *int = &b
fmt.Printf("Type of a is %T\n", a)
fmt.Println("address of b is", a)
}
&
操作符用来获取一个变量的地址。在上面的程序中,第 9 行我们将 b
的地址赋给 a
(a 的类型为 *int)。现在我们说 a
指向了 b
。当我们打印 a
的值时,b
的地址将会被打印出来。程序的输出为:
Type of a is *int
address of b is 0x1040a124
你可能得到的是一个不同的 b
的地址,因为 b
可以在内存中的任何地方。
指针的空值
指针的零值为nil
。(就不能跟其他语言一样用null吗)
package main
import (
"fmt"
)
func main() {
a := 25
var b *int
if b == nil {
fmt.Println("b is", b)
b = &a
fmt.Println("b after initialization is", b)
}
}
b 在上述程序中最初为 nil
,然后分配给 a
的地址。该程序输出
b is <nil>
b after initialisation is 0x1040a124
使用新函数创建指针
Go 还提供了一个方便的函数new
来创建指针。该new函数将一个类型作为参数并返回一个指针,该指针指向作为参数传递的类型的新分配的空值。
下面的例子可以说明清楚。
package main
import (
"fmt"
)
func main() {
size := new(int)
fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
*size = 85
fmt.Println("New size value is", *size)
}
在上面的程序中,在第8行。我们使用new
函数来创建类型的int
指针。该函数将返回一个指向新分配的int
类型的零值指针。int
类型的零值为0. 因此 size
将是 *int
类型并将指向0, *size
将为0。
上面的程序将打印
Size value is 0, type is *int, address is 0x414020
New size value is 85
指针解引用
解引用指针的意思是通过指针访问被指向的值。指针 a
的解引用表示为:*a
。
让我们看看这在程序中是如何工作的。
package main
import (
"fmt"
)
func main() {
b := 255
a := &b
fmt.Println("address of b is", a)
fmt.Println("value of b is", *a)
}
在上面程序的第 10 行,我们将 a
解引用,并打印了它的值。不出所料,它会打印出 b
的值。程序会输出:
address of b is 0x1040a124
value of b is 255
让我们再编写一个程序,用指针来修改 b
的值。
package main
import (
"fmt"
)
func main() {
b := 255
a := &b
fmt.Println("address of b is", a)
fmt.Println("value of b is", *a)
*a++
fmt.Println("new value of b is", b)
}
在上面程序的第 12 行中,我们把 a
指向的值加 1
,由于 a
指向了 b
,因此 b
的值也发生了同样的改变。于是 b
的值变为 256
。程序会输出:
address of b is 0x1040a124
value of b is 255
new value of b is 256
向函数传递指针参数
函数返回局部变量的指针是完全合法的。Go 编译器足够智能,它会在堆上分配这个变量。
package main
import (
"fmt"
)
func hello() *int {
i := 5
return &i
}
func main() {
d := hello()
fmt.Println("Value of d", *d)
}
上面程序的第9行,我们从hello
函数中返回局部变量的地址i
。此代码的行为在 C 和 C++ 等编程语言中是未定义的,因为一旦函数返回,变量i
就会超出范围。但是在 Go 的情况下,编译器会进行转义分析,并·在地址转义本地范围时在堆上进行分配。因此,该程序将起作用并打印:Value of d 5
Go 不支持指针运算
Go 不支持其他语言(如 C 和 C++)中存在的指针算法。
package main
func main() {
b := [...]int{109, 110, 111}
p := &b
p++
}
上述程序会抛出编译错误main.go:6: invalid operation: p++ (non-numeric type *[3]int)
(指针不能运算)
注意:
- 不要向函数传递数组的指针,而应该使用切片。你可以试着验证下为什么?
练习题:
通过指针变量 p 访问其成员变量 name,下面语法正确的是
(*p).name
结构体
本节重点:
- 学会并使用结构体
概念
数组、切片和 Map 可以用来表示同一种数据类型的集合,但是当我们要表示不同数据类型的集合时就需要用到结构体。
结构体是由零个或多个任意类型的值聚合成的实体,它可以用于将数据分组为一个单元而不是将它们中的每一个作为单独的值。
声明一个结构体
Go 里面用关键字 type
和 struct
用来定义结构体,语法如下:
type StructName struct{
FieldName type
}
我们来定义一个学生结构体:
package main
import "fmt"
type Student struct {
Age int
Name string
}
func main() {
stu := Student{
Age: 18,
Name: "name",
}
fmt.Println(stu)
// 在赋值的时候,字段名可以忽略
stu2 := Student{20, "new name"}
fmt.Println(stu2)
return
}
上述代码 5 行声明了一个Student
的结构体。而在第 11 行,通过指定每个字段名的值,我们定义了结构体变量 stu
。字段名的顺序不一定要与声明结构体类型时的顺序相同。在这里,我们改变了 Age
的位置,将其移到了末尾。这样做也不会有任何的问题。
但是在第 18 行。定义 stu2
时我们省略了字段名。在这种情况下,就需要保证字段名的顺序与声明结构体时的顺序相同。请不要使用这种语法,因为它很难确定哪个值是哪个字段的。我们在这里指定这种格式只是为了理解这也是一种有效的语法:)
上述代码输出如下:
{18 name}
{20 new name}
创建匿名结构体
可以在不创建新数据类型的情况下声明结构。这些类型的结构称为匿名结构。
package main
import (
"fmt"
)
func main() {
emp3 := struct {
firstName string
lastName string
age int
salary int
}{
firstName: "Andreah",
lastName: "Nikola",
age: 31,
salary: 5000,
}
fmt.Println("Employee 3", emp3)
}
在上述程序的第 8 行中,定义了一个匿名结构变量 emp3
。正如我们已经提到的,这个结构被称为匿名的,因为它只创建一个新的结构变量 emp3
,并没有像命名结构那样定义任何新的结构类型。
该程序输出:Employee 3 {Andreah Nikola 31 5000}
获取结构的各个字段
点.
运算符用于访问结构的各个字段。
package main
import (
"fmt"
)
type Employee struct {
firstName string
lastName string
age int
salary int
}
func main() {
emp6 := Employee{
firstName: "Sam",
lastName: "Anderson",
age: 55,
salary: 6000,
}
fmt.Println("First Name:", emp6.firstName)
fmt.Println("Last Name:", emp6.lastName)
fmt.Println("Age:", emp6.age)
fmt.Printf("Salary: $%d\n", emp6.salary)
emp6.salary = 6500
fmt.Printf("New Salary: $%d", emp6.salary)
}
上面程序中的emp6.firstName用于获取结构体emp6
的字段firstName
的内容。在第25行号,我们修改员工的工资。该程序打印:
First Name: Sam
Last Name: Anderson
Age: 55
Salary: $6000
New Salary: $6500
结构体的指针
也可以创建指向结构的指针。
package main
import (
"fmt"
)
type Employee struct {
firstName string
lastName string
age int
salary int
}
func main() {
emp8 := &Employee{
firstName: "Sam",
lastName: "Anderson",
age: 55,
salary: 6000,
}
fmt.Println("First Name:", (*emp8).firstName)
fmt.Println("Age:", (*emp8).age)
}
在上面程序中,emp8
是一个指向结构体 Employee
的指针。(*emp8).firstName
表示访问结构体 emp8
的 firstName
字段。该程序会输出:
First Name: Sam
Age: 55
Go 语言允许我们在访问 firstName
字段时,可以使用 emp8.firstName
来代替显式的解引用 (*emp8).firstName
。
package main
import (
"fmt"
)
type Employee struct {
firstName string
lastName string
age int
salary int
}
func main() {
emp8 := &Employee{
firstName: "Sam",
lastName: "Anderson",
age: 55,
salary: 6000,
}
fmt.Println("First Name:", emp8.firstName)
fmt.Println("Age:", emp8.age)
}
在上面的程序中,我们使用emp8.firstName
来访问 firstName
字段,该程序会输出:
First Name: Sam
Age: 55
匿名字段
可以创建具有仅包含类型而没有字段名称的字段的结构。这些类型的字段称为匿名字段。
下面的代码片段创建了一个结构Person
,它有两个匿名字段string
和int
type Person struct {
string
int
}
即使匿名字段没有明确的名称,默认情况下匿名字段的名称就是其类型的名称。例如,在上面的 Person
结构中,虽然字段是匿名的,但默认情况下它们采用字段类型的名称。所以Person
结构体有 2 个字段,分别为string
和int
.
package main
import (
"fmt"
)
type Person struct {
string
int
}
func main() {
p1 := Person{
string: "naveen",
int: 50,
}
fmt.Println(p1.string)
fmt.Println(p1.int)
}
在上面程序的第 17 行和第 18 行,我们访问了 Person
结构体的匿名字段,我们把字段类型作为字段名,分别为 “string” 和 “int”。上面程序的输出如下:
naveen
50
嵌套结构体
一个结构可能包含一个字段,而该字段又是一个结构。这些类型的结构称为嵌套结构。
package main
import (
"fmt"
)
type Address struct {
city string
state string
}
type Person struct {
name string
age int
address Address
}
func main() {
p := Person{
name: "Naveen",
age: 50,
address: Address{
city: "Chicago",
state: "Illinois",
},
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.address.city)
fmt.Println("State:", p.address.state)
}
上述程序中Person
的结构有一个字段address
,该字段address
又是一个结构。该程序打印:
Name: Naveen
Age: 50
City: Chicago
State: Illinois
注意:
- 当定义一个结构体变量,但是没有给它提供初始值,则对应的字段被赋予它们各自类型的零值
方法
本节重点:
- 学会方法的基本使用
方法主要源于 OOP 语言,在传统面向对象语言中 (例如 C++), 我们会用一个“类”来封装属于自己的数据和函数,这些类的函数就叫做方法。
虽然 Go 不是经典意义上的面向对象语言,但是我们可以在一些接收者(自定义类型,结构体)上定义函数,同理这些接收者的函数在 Go 里面也叫做方法。
方法的声明
方法(method)的声明和函数很相似, 只不过它必须指定接收者:
func (t T) F() {}
注意:
- 接收者的类型只能为用关键字
type
定义的类型,例如自定义类型,结构体。 - 同一个接收者的方法名不能重复 (没有重载),如果是结构体,方法名还不能和字段名重复。
- 值作为接收者无法修改其值,如果有更改需求,需要使用指针类型。
简单例子
现在我们写一个简单的程序,在结构类型上创建一个方法并调用它。
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee {
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary()
}
在上面程序的第 13 行中,我们在 Employee
结构体类型上创建了一个方法 displaySalary
。displaySalary()
方法可以访问其中的接收者 e
。我们使用收款人 e
,打印员工的 name
、currency
和 salary
。
我们在第23行使用该方法调用了 emp1.displaySalary()
上面程序的输出是 :Salary of Sam Adolf is $5000
指针接收器与值接收器型
前面都是带有值接收器的方法。可以使用指针接收器创建方法。值和指针接收器之间的区别在于,在具有指针接收器的方法内部所做的更改对调用者是可见的,而在值接收器中则不然。让我们在程序的帮助下理解这一点。
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)
fmt.Printf("\n\nEmployee age before change: %d", e.age)
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
在上面的程序中,changeName
方法有一个值接收器 (e Employee)
,而 changeAge
方法有一个指针接收器 (e *Employee)
。在 changeName
中对 Employee
结构体的 name
字段所做的更改对调用者是不可见的,因此程序会在方法 e
之前和之后打印相同的 name
。因为 changeAge
方法有一个指针接收器 (e *Employee)
,所以在方法调用 (&e)
之后对 age
字段所做的更改 (51)
将对调用者可见。这个程序打印:
Employee name before change: Mark Andrew
Employee name after change: Mark Andrew
Employee age before change: 50
Employee age after change: 51
何时使用指针接收与值接收
通常,当调用者应该可以看到方法内对接收器所做的更改时,可以使用指针接收器。
指针接收器也可用于复制数据结构成本高昂的地方。考虑一个具有许多字段的结构。将此结构用作方法中的值接收器将需要复制整个结构,这将是昂贵的。在这种情况下,如果使用指针接收器,则不会复制结构,而在方法中只会使用指向它的指针。
在所有其他情况下,可以使用值接收器。
命名冲突
a. 接收者定义的方法名不能重复, 例如:
package main
type T struct{}
func (T) F() {}
func (T) F(a string) {}
func main() {
t := T{}
t.F()
}
运行代码我们会得到 method redeclared: T.F
类似错误。
b. 结构体方法名不能和字段重复,例如:
package main
type T struct{
F string
}
func (T) F(){}
func main() {
t := T{}
t.F()
}
运行代码我们会得到 : type T has both field and method named F
类似错误。
同一个接收者的方法名不能重复 (没有重载);如果是结构体,方法名不能和字段重复。
接收者可以同时为值和指针
在 Go 语言中,方法的接收者可以同时为值或者指针,例如:
package main
type T struct{}
func (T) F() {}
func (*T) N() {}
func main() {
t := T{}
t.F()
t.N()
t1 := &T{} // 指针类型
t1.F()
t1.N()
}
可以看到无论值类型 T
还是指针类型 &T
都可以同时访问 F
和 N
方法。
值和指针作为接收者的区别
同样我们先看一段代码:
package main
import "fmt"
type T struct {
value int
}
func (m T) StayTheSame() {
m.value = 3
}
func (m *T) Update() {
m.value = 3
}
func main() {
m := T{0}
fmt.Println(m) // {0}
m.StayTheSame()
fmt.Println(m) // {0}
m.Update()
fmt.Println(m) // {3}
}
运行代码输出结果为:
{0}
{0}
{3}
值作为接收者(T
) 不会修改结构体值,而指针 *T
可以修改。
接口
本节重点:
- 学会 Go 中 interface 的基本使用
在 Go 中,接口类型是一种抽象类型,是方法的集合,其他类型实现了这些方法就是实现了这个接口。
声明和实现接口
在 Go 中接口的声明如下:
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
简单示例
现在我们通过一个简单的示例来看是如何创建接口并实现它:
package main
import (
"fmt"
)
//interface definition
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
//MyString 实现 VowelsFinder
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson")
var v VowelsFinder
v = name // possible since MyString implements VowelsFinder
fmt.Printf("Vowels are %c", v.FindVowels())
}
在上面程序第8行中,创建了一个名为 VowelsFinder
的接口类型,它有一个方法 FindVowels() []rune
。
在下一行中创建一个类型 MyString
它只是 string
的包装类。
在第15行中,我们将方法 FindVowels()[]rune
添加到接收方类型 MyStrin
g 中。现在 MyString
被认为实现了 VowelsFinder
接口。这与 Java 等其他语言非常不同,在 Java 中,类必须使用 implements
关键字显式地声明它实现了接口。如果类型包含接口中声明的所有方法,则 go 和 go 接口将隐式实现。
在第28行,我们将 MyString
类型的 name
赋给 v
类型的 VowelsFinder
。这是可能的,因为 MyString
实现了 VowelsFinder
。下一行调用 MyString
类型上的 FindVowels
方法,并打印字符串中所有的元音 Sam Anderson
。这个程序输出的 Vowels are [a e o]
这样已经创建并实现了第一个接口。
空接口
一个没有方法的接口称为空接口。它表示为 interface{}
。由于空接口没有任何方法,所以所有类型都实现空接口。
package main
import (
"fmt"
)
func describe(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}
func main() {
s := "Hello World"
describe(s)
i := 55
describe(i)
strt := struct {
name string
}{
name: "Naveen R",
}
describe(strt)
}
在上面的程序中第 7 行,describe(i interface{})
函数接受一个空接口作为参数,因此它可以传递任何类型。
我们将字符串、int 和 结构体分别传递给第 13、15 和 21 行中的 describe
函数。这个程序打印
类型断言
类型断言用于提取接口的基础值。
i.(T)
是用于获取具体类型为T
的接口i
的底层值的语法。
一个程序值一千字😀。让我们为类型断言写一个。
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int) //get the underlying int value from i
fmt.Println(s)
}
func main() {
var s interface{} = 56
assert(s)
}
第12行中的 s
的具体类型是 int
。我们使用第8行中的 i.(int)
语法来获取 i
的底层 int
值。这个程序打印 56
。
如果上面程序中的具体类型不是 int
会发生什么?好吧,让我们来了解一下。
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int)
fmt.Println(s)
}
func main() {
var s interface{} = "Steven Paul"
assert(s)
}
在上面的程序中,我们将具体类型string
的s
传递给assert
函数,试图从中提取 int
值。这个程序会因为这个问题而报错panic: interface conversion: interface {} is string, not int
。
为了解决上述问题,我们可以使用语法
v, ok := i.(T)
如果 i
的具体类型是 T
,那么 v
的值就是 i
, ok
为 true
。
如果 i
的具体类型不是 T
,那么 ok
将是 false
,v
将是类型 T
的零值,程序不会报错。
package main
import (
"fmt"
)
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = "Steven Paul"
assert(i)
}
当Steven Paul
被传递给assert
函数时,ok
将为 false
,因为i
具体类型不是int
,并且v
为 0
值。该程序将打印:
56 true
0 false
注意:
- 上面所有示例接口都是使用值接收器实现的。也可以使用指针接收器来实现接口。但是会有一些微妙的区别?动手试试!