背景
概述
Go 语言诞生于2007年9月30日,由三位大佬共同创造,他们分别是 Ken Thompson(贝尔实验室成员,Unix系统系统先驱,B语言创始人,C语言创始人),Rob Pike (贝尔实验室成员,Unix系统先驱,UTF-8 发明者之一,Go语言设计领头人),Robert Griesemer(JS V8引擎研发者,三人之中最年轻),还有一位是领头人的妻子Renee French,她主导设计了 Go 语言的 Logo,就是一只憨憨的土拨鼠。
经过了三年的初步设计与研发,Go 语言由谷歌公司于2009年11月10日正式以 BSD-3-Clause
协议开源,并推出了最初的版本,每半年发布一个二级版本,被称为21世纪的 C 语言。
时间轴
Go 语言从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想。
在 2008 年年中,Go 语言的设计工作接近尾声,一些员工开始以全职工作状态投入到这个项目的编译器和运行实现上。Ian Lance Taylor 也加入到了开发团队中,并于 2008 年 5 月创建了一个 gcc 前端。
Russ Cox 加入开发团队后着手语言和类库方面的开发,也就是 Go 语言的标准包。在 2009 年 10 月 30 日,Rob Pike 以 Google Techtalk 的形式第一次向人们宣告了 Go 语言的存在。
直到 2009 年 11 月 10 日,开发团队将 Go 语言项目以 BSD-style 授权(完全开源)正式公布了 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。
作为一个开源项目,Go 语言借助开源社区的有生力量达到快速地发展,并吸引更多的开发者来使用并改善它。大约在 2011 年 4 月 10 日,谷歌开始抽调员工进入全职开发 Go 语言项目。开源化的语言显然能够让更多的开发者参与其中并加速它的发展速度。Andrew Gerrand 在 2010 年加入到开发团队中成为共同开发者与支持者。
Go 语言的官方网站是 golang.org,这个站点采用 Python 作为后端,并且使用 Go 语言自带的工具 godoc 运行在 Google App Engine 上来作为 Web 服务器提供文本内容。在官网的首页有一个功能叫做 Go Playground,是一个 Go 代码的简单编辑器的沙盒,它可以在没有安装 Go 语言的情况下在个人浏览器中编译并运行 Go。
需求
在学习 Go 语言前,我们可能都会思考,在已经有 C/C++ 等语言的前提条件下为什么还需要一门新的语言?
那是因为 C/C++ 的发展速度无法跟上计算机发展的脚步,十多年来也没有出现一门与时代相符的主流系统编程语言,因此人们需要一门新的系统编程语言来弥补这个空缺,尤其是在计算机成为主流的信息时代。
对比计算机性能的提升,软件开发领域不被认为发展地足够快或者比硬件发展更加成功(有许多项目均以失败告终),同时应用程序的体积始终在不断地扩大,这就迫切地需要一门具备更高层次概念的低级语言来突破现状。
在 Go 语言出现之前,开发者们总是面临非常艰难的抉择,究竟是使用执行速度快但是编译速度并不理想的语言(比如 C++),还是使用编译速度较快但执行效率不佳的语言(如 Java),或者说开发难度较低但执行速度一般的动态语言呢?
显然,Go 语言在这 3 个条件之间做到了最佳的平衡,快速编译、高效执行、易于开发。
发展目标
Go 语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。
因此,Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。
Go 语言的另一个目标是对网络通信、并发和并行编程有着极佳的支持,从而更好地利用大量的分布式和多核的计算机,这一点对于谷歌内部的使用来说就非常重要了。设计者通过 goroutine 这种轻量级线程的概念来实现这个目标,然后通过 channel 来实现各个 goroutine 之间的通信。他们实现了分段栈增长和 goroutine 在线程基础上多路复用技术的自动化。
这个特性显然是 Go 语言最强有力的部分,不仅支持了日益重要的多核与多处理器计算机,也弥补了现存编程语言在这方面所存在的不足。
Go 语言中另一个非常重要的特性就是它的构建速度(即编译和链接到机器代码的速度),一般情况下构建一个程序的时间只需要数百毫秒到几秒。作为大量使用 C++ 来构建基础设施的谷歌来说,无疑从根本上摆脱了 C++ 在构建速度上非常不理想的噩梦。这不仅极大地提升了开发者的生产力,同时也使得软件开发过程中的代码测试环节更加紧凑,而不必浪费大量的时间在等待程序的构建上。
依赖管理是现今软件开发的一个重要组成部分,但是 C 语言中 “头文件” 的概念却导致越来越多一个大型项目因为依赖关系而使得构建需要长达几个小时的时间的情况出现。人们愈发需要一门具有严格的、简洁的依赖关系分析系统从而能够快速编译的编程语言。这正是 Go 语言采用包模型的根本原因,这个模型通过严格的依赖关系检查机制来加快程序构建的速度,提供了非常好的可量测性。
整个 Go 语言标准库的编译时间一般都在 20 秒以内,其它的常规项目也只需要半秒钟的时间来完成编译工作。这种闪电般的编译速度甚至比编译 C 语言或者 Fortran 更加快,使得编译这一环节不再成为在软件开发中困扰开发人员的问题。在这之前,动态语言将快速编译作为自身的一大亮点,像 C++ 那样的静态语言一般都有非常漫长的编译和链接工作。而同样作为静态语言的 Go 语言,通过自身优良的构建机制,成功地去除了这个弊端,使得程序的构建过程变得微不足道,拥有了像脚本语言和动态语言那样的高效开发的能力。
另外,Go 语言在执行速度方面也可以与 C/C++ 相提并论。
由于内存问题(通常称为内存泄漏)长期以来一直伴随着 C++ 的开发者们,Go 语言的设计者们认为内存管理不应该是开发人员所需要考虑的问题。因此尽管 Go 语言像其它静态语言一样执行本地代码,但它依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收(使用了一个简单的标记 - 清除算法)。
尽管垃圾回收并不容易实现,但考虑这将是未来并发应用程序发展的一个重要组成部分,Go 语言的设计者们还是完成了这项艰难的任务。
Go 语言还能够在运行时进行反射相关的操作。
使用 go install 能够很轻松地对第三方包进行部署。
此外,Go 语言还支持调用由 C 语言编写的海量库文件,从而能够将过去开发的软件进行快速迁移。
指导设计原则
Go 语言通过减少关键字的数量(25 个)来简化编码过程中的混乱和复杂度。干净、整齐和简洁的语法也能够提高程序的编译速度,因为这些关键字在编译过程中少到甚至不需要符号表来协助解析。
这些方面的工作都是为了减少编码的工作量,甚至可以与 Java 的简化程度相比较。
Go 语言有种极简抽象艺术家的感觉,因为它只提供了一到两种方法来解决某个问题,这使得开发者们的代码都非常容易阅读和理解。众所周知,代码的可读性是软件工程里最重要的一部分,因为代码是写给人看的,不是写给机器看的。
这些设计理念没有建立其它概念之上,所以并不会因为牵扯到一些概念而将某个概念复杂化,他们之间是相互独立的。
Go 语言有一套完整的编码规范,它不像 Ruby 那样通过实现过程来定义编码规范。作为一门具有明确编码规范的语言,它要求可以采用不同的编译器如 gc 和 gccgo 进行编译工作,这对语言本身拥有更好的编码规范起到很大帮助。
LALR 是 Go 语言的语法标准,你也可以在 src/cmd/internal/gc/go.y 中查看到,这种语法标准在编译时不需要符号表来协助解析。
特性
Go 语言从本质上(程序和结构方面)来实现并发编程。
因为 Go 语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性。Go 语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说这是一门混合型的语言。
在传统的面向对象语言中,使用面向对象编程技术显得非常臃肿,它们总是通过复杂的模式来构建庞大的类型层级,这违背了编程语言应该提升生产力的宗旨。
函数是 Go 语言中的基本构件,它们的使用方法非常灵活。
Go 语言使用静态类型,所以它是类型安全的一门语言,加上通过构建到本地代码,程序的执行速度也非常快。
作为强类型语言,隐式的类型转换是不被允许的,记住一条原则,让所有的东西都是显式的。Go 语言其实也有一些动态语言的特性(通过关键字 var),所以它对那些逃离 Java 和 .Net 世界而使用 Python、Ruby、PHP 和 JavaScript 的开发者们也具有很大的吸引力。
Go 语言支持交叉编译,比如说你可以在运行 Linux 系统的计算机上开发运行在 Windows 下运行的应用程序。这是第一门完全支持 UTF-8 的编程语言,这不仅体现在它可以处理使用 UTF-8 编码的字符串,就连它的源码文件格式都是使用的 UTF-8 编码,Go 语言做到了真正的国际化。
总结来说就是以下几点。
1、语法简单
Go语言在自由度和灵活度上做了取舍,以此换来了更好的维护性和平滑的学习曲线。
2、部署友好
Go静态编译后的二进制文件不依赖额外的运行环境,编译速度也非常快。
3、交叉编译
Go仅需要在编译时简单设置两个参数,就可以编译出能在其它平台上运行的程序。
4、天然并发
Go语言对于并发的支持是纯天然的,仅需一个关键字,就可以开启一个异步协程。
5、垃圾回收
Go有着优秀的GC性能,大部分情况下GC延时都不会超过1毫秒。
6、丰富的标准库
从字符串处理到源码AST解析,功能强大且丰富的标准库是Go语言坚实的基础。
7、完善的工具链
Go有着完善的开发工具链,涵盖了编译,测试,依赖管理,性能分析等方方面面。
Go 语言抛弃了继承,弱化了OOP,类,元编程,泛型,Lamda表达式等这些特性,拥有良好的性能和较低的上手难度,它非常适合用于云服务和应用服务端开发,甚至可以进行部分 Linux 嵌入式开发,不过由于带有垃圾回收,其性能始终无法媲美 C/C++ 这类系统级语言。Go 在其擅长的领域表现十分出色,虽然面世只有十余年不到,但已经有大量的行业将Go 作为了首选语言。Go语言仍有不少的缺点,比如令人诟病的错误处理,残缺的泛型,标准库虽然很丰富但内置的数据结构却没几个等等,不过总的来说这是一门非常年轻且具有活力的现代语言,未来发展势头很足。
用途
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
Go 语言一个非常好的目标就是实现所谓的复杂事件处理(CEP),这项技术要求海量并行支持,高度的抽象化和高性能。当我们进入到物联网时代,CEP 必然会成为人们关注的焦点。
但是 Go 语言同时也是一门可以用于实现一般目标的语言,例如对于文本的处理,前端展现,甚至像使用脚本一样使用它。
值得注意的是,因为垃圾回收和自动内存分配的原因,Go 语言并不适合用来开发对实时性要求很高的软件。越来越多的谷歌内部的大型分布式应用程序都开始使用 Go 语言来开发,例如谷歌地球的一部分代码就是由 Go 语言完成的。
在 Chrome 浏览器中内置了一款 Go 语言的编译器用于本地客户端(NaCl),这很可能会被用于在 Chrome OS 中执行 Go 语言开发的应用程序。Go 语言可以在 Intel 或 ARM 处理器上运行,因此它也可以在安卓系统下运行,例如 Nexus 系列的产品。
特性缺失
许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持,但其中的一部分可能会在未来被支持。
1、为了简化设计,不支持函数重载和操作符重载
2、为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
3、Go 语言通过另一种途径实现面向对象设计来放弃类和类型的继承
4、尽管在接口的使用方面可以实现类似变体类型的功能,但本身不支持变体类型
5、不支持动态加载代码
6、不支持动态链接库
7、不支持泛型
8、通过 recover 和 panic 来替代异常机制
9、不支持断言
10、不支持静态变量
优势总结
1、简化问题,易于学习
2、内存管理,简洁语法,易于使用
3、快速编译,高效开发
4、高效执行
5、并发支持,轻松驾驭
6、静态类型
7、标准类库,规范统一
8、易于部署
9、文档全面
10、免费开源
安装
这里因为主要讲go的语法和使用,所以安装部分会省略一些,大家可以可参考一些其他文档,比如这个入门指南 | Golang中文学习文档 (halfiisland.com)。
Windows
All releases - The Go Programming Languagehttps://go.dev/dl/
这是go官方网站,大家可以去里面直接下载,版本根据自己需要选择,如下图。
这里有两种类型可选,Installer 和 Archive,前者就是安装包,下载十分方便,不过推荐使用后者,因为这会让你更熟悉go语言的目录结构,未来出问题不至于手足无措。选择下载 zip 文件,压缩文件中包含 go 语言的源代码以及工具链和一些文档,将其解压指定的路径,然后需要配置两个系统环境变量。
下载后在解压设置系统变量前需要了解两个定义。
- GOROOT - go语言的安装路径,这是Go环境所做目录的配置
- GOPATH - go语言依赖存放路径,这个是Go项目的工作目录,以后代码的存放地址
设置好后,给系统环境变量 PATH 添加两条新的项,步骤如下。
最后在 powershell 中执行 go version 命令,最后能正常显示版本就说明安装正确。
PS C:\user\username> go version
go version go1.21.3 windows/amd64
更新的话只需要下载新的 zip 覆盖原安装目录即可。
linux
1、下载安装包
wget 下载网址(比如 https://golang.google.cn/dl/go1.21.1.linux-amd64.tar.gz)
//如果没有 wget 这个命令就使用 yum 下载或者直接用 curl -O(这里是英文大写O)
或者在官网上下载到本机后,用之前提到过的 xftp 传输到虚拟机指定目录下。
2、解压安装包
tar -C 下载指定目录路径 安装包名
3、设置环境变量
在 ~/.bashrc(这里的~就是家目录,也可以用$HOME代替)里添加环境变量。
export GOROOT=$HOME/go
export GOPATH=$HOME/gopath
export PATH=$PATH:$GOROOT/bin
设置完后需要 bash ~/.bashrc 确认修改生效。
编辑器
主流的 go 语言 IDE 比较推荐是下面两个,我个人目前主要用 goland。
Goland
jetbrain 出品,功能强大,全方位支持,不过需要付费,可以考虑 IDEA 社区版配合插件。
Vscode
无需付费,万能的编辑器,有插件加持什么语言都能写。
学习推荐
Go 语言之旅 (go-zh.org)https://tour.go-zh.org/welcome/1
这是由官方编写的一个非常简洁明了的教程,全中文支持,通过互动式的代码教学来帮助你快速了解 Go 语言的语法与特性,适合想要快速了解 Go 语言的人。
基本结构
go 语言的基本结构主要包括包声明、引入包、函数、变量、语句 & 表达式、注释。
用一个例子来举例。
package test // test是当前go文件属于的包的包名
import "fmt" // 导入包,后续实现fmt功能需要用的,其他功能就导入相应需要的包
// 这些都是注释,可以介绍一下下面函数的主要功能和作用
func main() {
var i, j int = 1, 2
a := i+j // 声明变量
fmt.Println(a)
}
package 关键字代表的是当前 go文件属于哪一个包,启动文件通常是 main 包,启动函数是 main 函数,在自定义包和函数时命名应当尽量避免与之重复。
import 是导入关键字,后面跟着的是被导入的包名。
func 是函数声明关键字,用于声明一个函数。
语句是指执行某种操作的完整指令,这里的 var i,j int = 1,2 和 a := i+j 都是语句。
表达式是指一个由操作数和操作符组合而成的计算单元,它可以计算出一个值。表达式可以出现在语句中,通常用于计算、比较或获取数据。这里的 i+j 就是一个表达式。
fmt.Println(a)是一个语句,调用了 fmt 包下的 Prinln 函数进行控制台输出。
基础语法
包
在 Go 中,程序是通过将包链接在一起来构建的,也可以理解为最基本的调用单位是包,而不是 go 文件。包其实就是一个文件夹,包内共享所有源文件的变量,常量,函数以及其他类型。包的命名风格建议都是小写字母,并且要尽量简短。
导入
比如创建一个 example 包,包下有如下函数。
package example
import "fmt"
func SayHello() {
fmt.Println("Hello")
}
在 main 函数中调用。
package main
import "example"
func main() {
example.SayHello()
}
还可以给包起别名。
package main
import e "example"
func main() {
e.SayHello()
}
批量导入时,可以使用括号()来表示。
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.MaxInt64)
}
也可以只导入不调用,通常这么做是为了调用该包下的 init 函数。
package main
import (
"fmt"
_ "math" // 下划线表示匿名导入
)
func main() {
fmt.Println(1)
}
在 Go 中完全禁止循环导入,不管是直接的还是间接的。比如包 A 导入了包 B,包 B 也导入了包 A,这是直接循环导入,包 A 导入了包 C,包 C 导入了包 B,包 B 又导入了包 A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。
导出
在 Go 中,导出和访问控制是通过命名来进行实现的,如果想要对外暴露一个函数或者一个变量,只需要将其名称首字母大写即可,比如 example 包下的 Test 函数。
package example
import "fmt"
// 首字母大写,可以被包外访问
func Test() {
fmt.Println("Hello")
}
如果想要不对外暴露的话,只需将名称首字母改为小写即可。
package example
import "fmt"
// 首字母小写,外界无法访问
func test() {
fmt.Println("Hello")
}
对外暴露的函数和变量可以被包外的调用者导入和访问,如果是不对外暴露的话,那么仅包内的调用者可以访问,外部将无法导入和访问,该规则适用于整个 Go 语言,比如后面的结构体及其字段、方法、自定义类型、接口等等。
注释
Go 支持单行注释和多行注释,注释与内容之间建议隔一个空格。
// 这是main包
package main
// 导入了fmt包
import "fmt"
/*
这是启动函数main函数
*/
func main() {
// 这是一个语句
fmt.Println("hello everyone!")
}
标识符
标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下。
1、只能由字母,数字,下划线组成
2、只能以字母和下划线开头
3、严格区分大小写
4、不能与任何已存在的标识符重复,即包内唯一的存在
5、不能与Go任何内置的关键字冲突
下方列出了几乎所有的内置关键字。
break | default | func | interface | select |
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
之所以刻意地将 Go 代码中的关键字保持的这么少,是为了简化在编译过程第一步中的代码解析。和其它语言一样,关键字不能够作标识符使用。
除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数。
预定义标识符如下。
append | bool | byte | cap | close | complex |
complex64 | complex128 | uint16 | copy | false | float32 |
float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new |
nil | panic | uint64 | println | real | |
recover | string | true | uint | uint8 | uintptr |
程序一般由关键字、常量、变量、运算符、类型和函数组成。
程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。
程序中可能会使用到这些标点符号,“.”、“,”、“;”、“:” 和“…”。
程序的代码通过语句来实现结构化。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
运算符
Go 语言内置的运算符有算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符。
这些运算符都比较常见,所以就不再展开介绍,只简单介绍下格式化说明符。
在格式化字符串里 %d 用于格式化整数(%x 和 %X 用于格式化16 进制表示的数字),%g 用于格式化浮点型( %f 输出浮点数,%e 输出科学计数表示法),%0d 用于规定输出定长的整数(其中开头的数字 0 是必须的)。
%n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,比如使用格式化字符串 %5.2e 来输出 3.4 的结果为 3.40e+00。
下面是 Go 语言中支持的运算符号的优先级排列。
Precedence(优先级) | Operator(运算符) |
---|---|
5 | * 、/ 、% 、<< 、>> 、& 、&^ |
4 | + 、- 、| 、^ |
3 | == 、!= 、< 、<= 、> 、>= |
2 | && |
1 | || |
有一点需要稍微注意下,go 语言中没有选择将 ~ 作为取反运算符,而是复用了 ^ 符号,当两个数字使用 ^ 时,例如 a^b,它就是异或运算符;只对一个数字使用时,比如 ^a,那么它就是取反运算符。
Go 语言中没有自增与自减运算符,它们被降级为了语句 statement,并且规定了只能位于操作数的后方,所以不用再去纠结 i++和++i 这样的问题。
a++ // 正确
++a // 错误
a-- // 正确
还有一点就是,它们不再具有返回值,因此 a=b++ 这类语句的写法是错误的。
字面量
字面量,按照计算机科学的术语来讲是用于表达源代码中一个固定值的符号,也叫字面值。两个叫法都是一个意思,写了什么东西,值就是什么,值就是“ 字面意义上 ”的值。
整型字面量
为了便于阅读,允许使用下划线_
来进行数字划分,但是仅允许在前缀符号之后和数字之间使用。
24 // 24
024 // 24
2_4 // 24
0_2_4 // 24
0O24 // 20
10_000 // 10k
100_000 // 100k
0b00 // 0
0x00 // 0
0x0_0 // 0
浮点数字面量
通过不同的前缀可以表达不同进制的浮点数。
0.
72.40
072.40 // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5. // == 15.0
0.15e+0_2 // == 15.0
0x1p-2 // == 0.25
0x2.p10 // == 2048.0
0x1.Fp+0 // == 1.9375
0X.8p-0 // == 0.5
0X_1FFFP-16 // == 0.1249847412109375
复数字面量
0i
0123i // == 123i
0o123i // == 0o123 * 1i == 83i
0xabci // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i // == 0x1p-2 * 1i == 0.25i
字符字面量
字符字面量必须使用单引号括起来 ' ',Go 中的字符完全兼容UTF-8。
'a'
'ä'
'你'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
转义字符
Go 中可用的转义字符。
\a //U+0007 响铃符号(建议调高音量)
\b //U+0008 回退符号
\f //U+000C 换页符号
\n //U+000A 换行符号
\r //U+000D 回车符号
\t //U+0009 横向制表符号
\v //U+000B 纵向制表符号
\\ //U+005C 反斜杠转义
\'a' //U+0027 单引号转义 (该转义仅在字符内有效)
\"a" //U+0022 双引号转义 (该转义仅在字符串内有效)
字符串字面量
字符串字面量必须使用双引号 " " 括起来或者反引号(反引号字符串不允许转义)。
`abc` // "abc"
`\n
\n` // "\\n\n\\n"
"\n"
"\"" // `"`
"Hello, world!\n"
"今天天气不错"
编码风格
关于编码风格 Go 是强制所有人统一同一种风格,Go 官方提供了一个格式化工具 gofmt,通过命令行就可以使用,该格式化工具没有任何的格式化参数可以传递,仅有的两个参数也只是输出格式化过程,所以完全不支持自定义,也就是说所有通过此工具的格式化后的代码都是同一种代码风格。
下面会简单列举一些规则,平时在编写代码的时候也可以稍微注意一下。
1、花括号,关于花括号 { } 到底该不该换行,几乎每个程序员都能说出属于自己的理由,其实最好在 Go中所有的花括号都不换行。
// 正确示例
func main() {
fmt.Println("a")
}
// 错误示例
func main()
{
fmt.Println("a")
}
2、缩进,Go默认使用 Tab 也就是制表符进行缩进,仅在一些特殊情况会使用空格。
3、间隔,Go中大部分间隔都是有意义的,从某种程度上来说,这也代表了编译器是如何看待你的代码的。
1*3 - 3*1
在 Go 语言中,空格通常用于分隔标识符、关键字、运算符和表达式,以提高代码的可读性。
Go 语言中变量的声明必须使用空格隔开。
var a int
const Pi float64 = 4.7384682
在运算符和操作数之间要使用空格能让程序更易阅读。
a = b + c;
在函数调用时,函数名和左边等号之间要使用空格,参数之间也要使用空格。
test := new(8)
众所周知,乘法的优先级比加法要高,在格式化后,* 符号之间的间隔会显得更紧凑,意味着优先进行运算,而 + 符号附近的间隔则较大,代表着较后进行运算。
基本数据类型
布尔类型
布尔类型只有真值和假值。
类型 | 描述 |
---|---|
bool | true为真值,false为假值 |
在 Go 中,整数 0 并不代表假值,非零整数也不能代表真值,即数字无法代替布尔值进行逻辑判断,两者是完全不同的类型。
Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较。
布尔型的常量和变量也可以通过和逻辑运算符(非 !、和 &&、或 ||)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的 Go 语句。
布尔值(以及任何结果为布尔值的表达式)最常用在条件结构的条件语句中,比如 if、for 和 switch 结构。
对于布尔值而言,好的命名能够很好地提升代码的可读性。比如以 is或者Is开头的 isSorted、isFinished、isVisible,使用这样的命名能够在阅读代码的获得阅读正常语句一样的良好体验。
整型
Go 中为不同位数的整数分配了不同的类型,主要分为无符号整型与有符号整型。
序号 | 类型和描述 | 序号 | 类型和描述 |
---|---|---|---|
uint8 | 无符号 8 位整型 | uint16 | 无符号 16 位整型 |
uint32 | 无符号 32 位整型 | uint64 | 无符号 64 位整型 |
int8 | 有符号 8 位整型 | int16 | 有符号 16 位整型 |
int32 | 有符号 32 位整型 | int64 | 有符号 64 位整型 |
uint | 无符号整型 至少32位 | int | 整型 至少32位 |
还有 uintptr,等价于无符号64位整型,但是专用于存放指针运算,用于存放死的指针地址。
浮点型
IEEE-754 浮点数,主要分为单精度浮点数与双精度浮点数。
类型 | 类型和描述 |
---|---|
float32 | IEEE-754 32位浮点数 |
float64 | IEEE-754 64位浮点数 |
复数类型
类型 | 描述 |
---|---|
complex128 | 64位实数和虚数 |
complex64 | 32位实数和虚数 |
复数使用 re+imI 来表示,其中 re 代表实数部分,im 代表虚数部分,I 代表根号负 1。
var x complex64 = 5 + 10i
fmt.Printf("a is: %v", x)
// 输出: 5 + 10i
如果 re 和 im 的类型均为 float32,那么类型为 complex64 的复数 c 输入如下。
a = complex(re, im)
// 函数 real(a) 和 imag(a) 可以分别获得相应的实数和虚数部分。
复数支持和其它数字类型一样的运算。我们使用等号 == 或者不等号 != 对复数进行比较运算时,需要注意对精确度的把握。
字符类型
Go 语言字符串完全兼容 UTF-8。
类型 | 描述 |
---|---|
byte | 等价 uint8 可以表达ANSCII字符 |
rune | 等价 int32 可以表达Unicode字符 |
string | 字符串即字节序列,可以转换为 []byte 类型即字节切片 |
派生类型
类型 | 例子 | 类型 | 例子 |
---|---|---|---|
数组 | [3]int - 长度为3的整型数组 | 切片 | []float64 - 64位浮点数切片 |
映射表 | map[string]int - 键为字符串类型,值为整型的映射表 | 结构体 | type A struct {} - 名为A的结构体 |
指针 | *int - 一个整型指针 | 函数 | type A func () - 一个没有参数,没有返回值的函数类型 |
接口 | type A interface {} - A接口 | 通道 | chan int - 整型通道 |
零值
官方文档中零值称为 zero value,零值并不仅仅只是字面上的数字零,而是一个类型的空值或者说默认值更为准确。
类型 | 零值 | 类型 | 零值 |
---|---|---|---|
数字类型 | 0 | 布尔类型 | false |
数组 | 固定长度的对应类型的零值集合 | 字符串类型 | "" |
结构体 | 内部字段都是零值的结构体 | 切片,映射表,函数,接口,通道,指针 | nil |
nil
源代码中的 nil,可以看出 nil 仅仅只是一个变量。
var nil Type
Go 中的 nil 不等同于其他语言的 null,nil 仅仅只是一些类型的零值,并且不属于任何类型,所以 nil == nil 这样的语句是无法通过编译的。
常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型,不能是除基本类型以外的其它类型,如结构体,接口,切片,数组等,也不能是函数的返回值。
常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于字面量、其他常量标识符、常量表达式、结果是常量的类型转换、iota等。
初始化
常量的声明需要用到 const 关键字,常量在声明时就必须初始化一个值,并且常量的类型可以省略。
const name string = "Jack" // 字面量
const msg = "hello world" // 字面量
const num = 1 // 字面量
const numExpression = (1+2+3) / 2 % 100 + num // 常量表达式
如果仅仅只是声明而不指定值,将会无法通过编译。
批量声明常量可以用 () 括起来以提升可读性,可以存在多个 () 达到分组的效果。
const (
a = 1
b = "start"
)
在同一个常量分组中,在已经赋值的常量后面的常量可以不用赋值,其值默认就是前一个的值。
const (
A = 1
B // 1
C // 1
)
iota
iota 是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。
const iota = 0
const (
Num = iota // 0
Num1 // 1
Num2 // 2
)
也可以这样写。
const (
Num = iota*2 // 0
Num1 // 2
Num2 // 4
)
还可以这样写。
const (
Num = iota << 2*3 + 1 // 1
Num1 // 13
Num2 // 25
Num3 = iota // 3
Num4 // 4
)
通过上面几个例子可以发现, iota 是递增的,第一个常量使用 iota 值的表达式,根据序号值的变化会自动的赋值给后续的常量,直到用新的 iota 重置,这个序号就是代码的相对行号,是相对于当前分组的起始行号, iota 的值本质上就是 iota 所在行相对于当前 const 分组的第一行的差值。而不同的 const 分组则相互不会影响。
注意
常量的值无法被修改,如果尝试对其进行修改的话将会无法通过编译。
数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出。不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。
变量
变量是用于保存一个值的存储位置,允许其存储的值在运行时动态的变化。每声明一个变量,都会为其分配一块内存以存储对应类型的值。
声明
在 go 中的类型声明是后置的,变量的声明会用到 var 关键字,格式为 var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则。
需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后,那么 Go 为什么要选择这么做呢?
首先,它是为了避免像 C 语言中那样含糊不清的声明形式,比如 int* a, b; 。在这个例子中,只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写(你可以在 Go 语言的声明语法 页面找到有关于这个话题的更多讨论)。
而在 Go 中,则可以很轻松地将它们都声明为指针类型 ,var a, b *int 。
其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解。
var a int
var b string
var c byte
当要声明多个相同类型的变量时,可以只写一次类型。
var A, B, C int
当要声明多个不同类型的变量时,可以使用()
进行包裹,可以存在多个()
。
var (
name string
age int
sex string
)
一个变量如果只是声明而不赋值,那么变量存储的值就是对应类型的零值。
当一个变量被声明之后,系统自动赋予它该类型的零值,int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。
变量的命名规则最好遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,比如像numShips 和 startDate。
赋值
赋值会用到运算符 = ,如下。
var a string
a = "alice"
也可以声明的时候直接赋值。
var a string = "alice"
也可以这样写。
var a string
var b int
a, b = "alice", 10
第二种方式每次都要指定类型,可以使用官方提供的语法":" - 短变量初始化,省略掉 var 关键字和后置类型,具体是什么类型交给编译器自行推断。
a := "alice" // 字符串类型的变量
虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种就不行。
a := 10
a = "alice"
还需要注意的是,短变量初始化不能使用 nil
,因为 nil
不属于任何类型,编译器无法推断其类型。
a := nil // 无法通过编译
短变量声明可以批量初始化。
a, b := "alice", 10
短变量声明方式无法对一个已存在的变量使用。
// 错误示例
var a int
a := 1
a := 2
但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量。
a := 1
a, b := 2, 2
这种代码是可以通过编译的,变量 a 被重新赋值,而 b 是新声明的。
在 go 语言中,有一个规则,那就是所有在函数中的变量都必须要被使用。
func main() {
a := 1
}
上面这种情况在编译时就会报错,提示你这个变量声明了但没有使用。
这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译。
var a = 1
func main() {
}
交换
在 Go 中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观。
a, b := 1, 2
a, b = b, a
由于在函数内部存在未使用的变量会无法通过编译,但有些变量又确实用不到,这个时候就可以使用匿名变量 _,使用来 _ 表示该变量可以忽略。
a, b, _ := 1, 2, 3
比较
变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go 语言中不存在隐式类型转换,下面这种就不行。
func main() {
var a uint64
var b int64
fmt.Println(a == b)
}
想要比较必须使用强制类型转换。
func main() {
var a uint64
var b int64
fmt.Println(int64(a) == b)
}
在没有泛型之前,早期go提供的内置 min,max 函数只支持浮点数,到了1.21版本,go 才终于将这两个内置函数用泛型重写。
minVal := min(1, 6, -8, 2.2) // 比较最小值
maxVal := max(10, 22, -5, 1.8) // 比较最大值
它们的参数支持所有的可比较类型,go中的可比较类型有布尔、数字、字符串、指针、通道 (仅支持判断是否相等)、元素是可比较类型的数组(切片不可比较)、字段类型都是可比较类型的结构体(仅支持判断是否相等)。
除此之外,还可以通过导入标准库来 cmp 判断,不过仅支持有序类型的参数,在 go 中内置的有序类型只有数字和字符串。
import "cmp"
func main() {
cmp.Compare(1, 2)
cmp.Less(1, 2)
}
代码块
在函数内部,可以通过 {} 建立一个代码块,代码块彼此之间的变量作用域是相互独立的。
func main() {
a := 1
{
a := 2
fmt.Println(a)
}
{
a := 3
fmt.Println(a)
}
fmt.Println(a)
}
结果依次输出2、3、1。
块与块之间的变量相互独立,不受干扰,无法访问,但是子块会受到父块中的影响。
func main() {
a := 1
{
a := 2
fmt.Println(a)
}
{
fmt.Println(a)
}
fmt.Println(a)
}
这次的输出依次是2、1、1。
值类型和引用类型
程序中所用到的内存在计算机中使用一堆箱子来表示,这些箱子被称为 “ 字 ”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度,所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。
所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。
当使用等号 = 将一个变量的值赋值给另一个变量时,如 a = b,实际上是在内存中将 a 的值进行了拷贝。
我们可以通过 &i 来获取变量 i 的内存地址,比如 0xf840000040(每次地址都可能不一样),值类型的变量的值存储在栈中。
内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
这个内存地址被称之为指针,这个指针实际上也被存在另外的某一个字中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 a1 = a2 时,只有引用(地址)被复制。
如果 a1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,a2也会受到影响。
在 Go 语言中,指针属于引用类型,其它的引用类型还包括 slices,maps 和 channel ,被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
init函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。
init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 backend()。
func init() {
// setup preparations
go backend()
}
字符串
字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节,这与其它语言如 C++、Java 或者 Python 不同(Java 始终使用 2 个字节)。
Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
简单来说,字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是字节的定长数组。
Go 支持解释字符串和非解释字符串的字面值。
解释字符串,该类字符串使用双引号括起来,其中的相关的转义字符将被替换,包括 \n(换行符)、\r(回车符)、\t(tab 键)、\u 或 \U(Unicode 字符)、\\(反斜杠自身)。
非解释字符串,该类字符串使用反引号括起来,支持换行。
`This is a raw string \n` // `\n\` 会被原样输出。
和 C/C++ 不一样,Go 中的字符串是根据长度限定,而非特殊字符 \0。
string 类型的零值为长度为零的字符串,即空字符串 ""。
一般的比较运算符(==、!=、<、<=、>=、>)通过在内存中按字节比较来实现字符串的对比。你可以通过函数 len() 来获取字符串所占的字节长度,比如 len(str)。
而字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数。
str[0] //字符串 str 的第 1 个字节
str[i - 1] //第 i 个字节
str[len(str)-1] //最后 1 个字节
//需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意,获取字符串中某个字节的地址的行为是非法的,比如 &str[i] 。
字面量
前面提到过字符串有两种字面量表达方式,分为普通字符串和原生字符串。
普通字符串
普通字符串由 "" 双引号表示,支持转义,不支持多行书写,下面是一些普通字符串。
"普通字符串\n"
"yhxrzegzthjn\t\\udz"
原生字符串
原生字符串由反引号 `` 表示,不支持转义,支持多行书写,原生字符串里面所有的字符都会原封不动的输出,包括换行和缩进。
`原生字符串,换行
tab缩进,\t制表符但是无效,换行
`
访问
因为字符串本质是字节数组,所以字符串的访问形式跟数组切片完全一致。
func main() {
str := "this is a string"
fmt.Println(str[0])
}
// 这行代码打印字符串 str 中索引为 0 的字符。在 Go 中,字符串是由字节组成的,str[0] 返回的是字符串的第一个字节(字符)。这里 str[0] 对应的字符是 't'(字符 t 的 ASCII 值为 116)。
输出是字节而不是字符。
fmt.Println(string(str[0])) // 想要输出字符 't'的话就得用 fmt 包的 Println 函数
切割
func main() {
str := "this is a string"
fmt.Println(string(str[0:4]))
}
// 打印字符串 str 从索引 0 到 3 的子字符串,这里是 this
字符串定义好后无法修改,只能覆盖。
转换
字符串可以转换为字节切片,而字节切片或字节数组也可以转换为字符串。
func main() {
str := "this is a string"
// 显式类型转换为字节切片,每个字符在字节切片中被表示为其对应的 ASCII 值
bytes := []byte(str)
fmt.Println(bytes)
// 输出[116 104 105 115 32 105 115 32 97 32 115 116 114 105 110 103]
// 显式类型转换为字符串
fmt.Println(string(bytes))
// 输出this is a string
}
字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的。
func main() {
str := "this is a string"
fmt.Println(&str)
bytes := []byte(str)
// 修改字节切片
bytes = append(bytes, 96, 97, 98, 99)
// 赋值给原字符串
str = string(bytes)
fmt.Println(str)
}
注意,两种类型之间的转换都需要进行数据拷贝,其性能损耗会随着长度的增加而增长。
长度
字符串的长度,其实并不是字面量的长度,而是字节数组的长度,只是大多数时候都是 ASCII 字符,刚好能用一个字节表示,所以恰好与字面量长度相等,求字符串长度使用内置函数 len 。
func main() {
str := "say hello" // 长度是9
str2 := "字符串" // 长度是18
fmt.Println(len(str), len(str2))
}
中文字符串看起来比英文字符串短,但实际长度却比英文字符串长。这是因为在 unicode 编码中,一个汉字在大多数情况下占3个字节,一个英文字符只占一个字节。
拷贝
类似数组切片的拷贝方式,字符串拷贝其实是字节切片拷贝,使用内置函数 copy。
func main() {
var x, a string // 声明两个字符串变量 x 和 a
a = "this is a string" // 给变量 a 赋值
a1 := make([]byte, len(a)) // 创建一个字节切片 a1,长度与字符串 a 相同
copy(a1, a) // 将字符串 a 的内容复制到字节切片 a1
x = string(a1) // 将字节切片 a1 转换回字符串并赋值给 x
fmt.Println(a, x) // 打印原字符串 a 和复制后的字符串 x
}
同样也可以使用 string.clone 函数,但其实内部实现都大同小异。
func main() {
var x, a string
a = "this is a string"
x = strings.Clone(a) // 使用 strings.Clone 函数复制 a 的内容到 x
fmt.Println(a, x)
}
拼接
字符串拼接符 +,两个字符串如 s1 和 s2 可以通过 s := s1 + s2 拼接在一起,s2 追加在s1 尾部并生成一个新的字符串 s。
拼接的简写形式 += 也可以用于字符串。
s := "wel" + "come,"
s += "hi!"
fmt.Println(s) //输出 "welcome,hi!"
在循环中使用加号 + 拼接字符串不是最高效的做法,更好的办法是使用函数 strings.Join(),d但也有更好的办法,比如使用字节缓冲(bytes.Buffer)拼接。
在 Go 中,字符串本质上是一个不可变的只读的字节数组,也是一片连续的内存空间。
也可以转换为字节切片再进行添加元素。
func main() {
str := "this is a string"
bytes := []byte(str)
bytes = append(bytes, "that is a int"...)
str = string(bytes)
fmt.Println(str)
}
以上两种拼接方式性能都很差,一般情况下可以使用,但如果对应性能有更高要求,可以使 用 strings.Builder。
func main() {
builder := strings.Builder{}
builder.WriteString("this is a string ")
builder.WriteString("that is a int")
fmt.Println(builder.String())
}
遍历
Go 中的字符串就是一个只读的字节切片,也就是说字符串的组成单位是字节而不是字符,这种情况经常会在遍历字符串时遇到。
func main() {
str := "hello"
for i := 0; i < len(str); i++ {
fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
}
}
上面分别输出了字节的十进制形式和十六进制形式。
104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
由于例子中的字符都是属于ASCII字符,只需要一个字节就能表示,所以结果恰巧每一个字节对应一个字符。但如果包含非ASCII字符结果就不同了。
func main() {
str := "世界"
for i := 0; i < len(str); i++ {
fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
}
}
通常情况下,一个中文字符会占用3个字节,所以就可能会看到乱码。
228,e4,ä
184,b8,¸
150,96,
231,e7,ç
149,95,
140,8c,
按照字节来遍历会把中文字符拆开,这显然会出现乱码。Go字符串是明确支持 UTF-8 的,应对这种情况就需要用到 rune 类型,在使用 for range 进行遍历时,其默认的遍历单位类型就是一 个 rune。
func main() {
str := "世界"
for _, r := range str {
fmt.Printf("%d,%x,%s\n", r, r, string(r))
}
}
输出如下。
19990,4e16,世
30028,754c,界
rune 本质上是 int32 的类型别名,Unicode 字符集的范围位于0x0000 - 0x10FFFF之间,最大也只有3个字节,合法的 UTF8 编码最大字节数只有4个字节,所以使用 int32 来存储是理所当然,上述例子中将字符串转换成 []rune 再遍历也是一样的道理。
func main() {
str := "世界"
runes := []rune(str)
for i := 0; i < len(runes); i++ {
fmt.Println(string(runes[i]))
}
}
strings 和 strconv 包(字符串操作)
作为一种基本数据结构,每种语言都有一些对于字符串的预定义处理函数,Go 中使用 strings 包来完成对字符串的主要操作。
前缀和后缀
// HasPrefix 判断字符串 s 是否以 ok 开头
strings.HasPrefix(s, ok string) bool
// HasSuffix 判断字符串 s 是否以 ok 结尾
strings.HasSuffix(s, ok string) bool
字符串包含关系
// Contains 判断字符串 s 是否包含 ok:
strings.Contains(s, ok string) bool
判断子字符串或字符在父字符串中出现的位置(索引)
// Index 返回字符串 str 在字符串 s 中的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str
strings.Index(s, str string) int
// LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str
strings.LastIndex(s, str string) int
// 如果 ch 是非 ASCII 编码的字符,建议使用以下函数来对字符进行定位
strings.IndexRune(s string, r rune) int
字符串替换
// Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new
strings.Replace(str, old, new string, n int) string
统计字符串出现次数
// Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数
strings.Count(s, str string) int
重复字符串
// Repeat 用于重复 count 次字符串 s 并返回一个新的字符串
strings.Repeat(s, count int) string
修改字符串大小写
// ToLower 将字符串中的 Unicode 字符全部转换为相应的小写字符
strings.ToLower(s) string
// ToUpper 将字符串中的 Unicode 字符全部转换为相应的大写字符
strings.ToUpper(s) string
修剪字符串
strings.TrimSpace(s) // 剔除字符串开头和结尾的空白符号
strings.Trim(s, "cut") //剔除指定字符,这里是将开头和结尾的 cut 去除掉,该函数的第二个参数可以包含任何字符,如果只想剔除开头或者结尾的字符串,则可以使用 TrimLeft 或者 TrimRight 来实现
分割字符串
strings.Fields(s)
// 利用空白作为分隔符将字符串分割为若干块,并返回一个 slice 。如果字符串只包含空白符号,返回一个长度为 0 的 slice
strings.Split(s, sep)
// 自定义分割符号对字符串分割,返回 slice
// 因为这 2 个函数都会返回 slice,所以习惯使用 for-range 循环来对其进行处理
拼接 slice 到字符串
// Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串
strings.Join(sl []string, sep string) string
举例如下。
package main
import (
"fmt"
"strings"
)
func main() {
str := "The quick brown fox jumps over the lazy dog"
sl := strings.Fields(str)
fmt.Printf("Splitted in slice: %v\n", sl)
for _, val := range sl {
fmt.Printf("%s - ", val)
}
fmt.Println()
str2 := "GO1|The ABC of Go|25"
sl2 := strings.Split(str2, "|")
fmt.Printf("Splitted in slice: %v\n", sl2)
for _, val := range sl2 {
fmt.Printf("%s - ", val)
}
fmt.Println()
str3 := strings.Join(sl2,";")
fmt.Printf("sl2 joined by ;: %s\n", str3)
}
// 输出
// Splitted in slice: [The quick brown fox jumps over the lazy dog]
// The - quick - brown - fox - jumps - over - the - lazy - dog -
// Splitted in slice: [GO1 The ABC of Go 25]
// GO1 - The ABC of Go - 25 -
// sl2 joined by ;: GO1;The ABC of Go;25
从字符串中读取内容
// 函数 strings.NewReader(str) 用于生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有 Read() 从 [] byte 中读取内容;ReadByte() 和 ReadRune() 从字符串中读取下一个 byte 或者 rune。
字符串与其它类型的转换
与字符串相关的类型转换都是通过 strconv 包实现的。
任何类型 T 转换为字符串总是成功的。
针对从数字类型转换到字符串,Go 提供了以下函数。
strconv.Itoa(i int) string
// 返回数字 i 所表示的字符串类型的十进制数
strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string
// 将 64 位浮点型的数字转换为字符串,其中 fmt 表示格式(其值可以是 'b'、'e'、'f' 或 'g'),prec 表示精度,bitSize 则使用 32 表示 float32,用 64 表示 float64
// 将字符串转换为其它类型 tp 并不总是可能的,可能会在运行时抛出错误 parsing "…": invalid argument
针对从字符串类型转换为数字类型,Go 提供了以下函数。
strconv.Atoi(s string) (i int, err error)
// 将字符串转换为 int 型
strconv.ParseFloat(s string, bitSize int) (f float64, err error)
// 将字符串转换为 float64 型
// 利用多返回值的特性,这些函数会返回 2 个值,第 1 个是转换后的结果(如果转换成功),第 2 个是可能出现的错误,因此,我们一般使用以下形式来进行从字符串到其它类型的转换:val, err = strconv.Atoi(s)
输入输出
标准
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
在 os 包下有三个外暴露的文件描述符,其类型都是*File ,分别是Stdin - 标准输入、Stdout - 标准输出、Stderr- 标准错误,Go 中的控制台输入输出都离不开它们。
输出
输出一句 Hello ,比较常用的有三种方法,第一种是调用 os.Stdout 。
os.Stdout.WriteString("Hello")
第二种是使用内置函数 println 。
println("Hello")
第三种也是最推荐的一种就是调用 fmt 包下的 Println 函数。
fmt.Println("Hello")
fmt.Println 会用到反射,因此输出的内容通常更容易使人阅读,不过性能一般。
格式化
0 | 格式化 | 描述 | 接收类型 |
---|---|---|---|
1 | %% | 输出百分号% | 任意类型 |
2 | %s | 输出string /[] byte 值 | string ,[] byte |
3 | %q | 格式化字符串,输出的字符串两端有双引号"" | string ,[] byte |
4 | %d | 输出十进制整型值 | 整型类型 |
5 | %f | 输出浮点数 | 浮点类型 |
6 | %e | 输出科学计数法形式 ,也可以用于复数 | 浮点类型 |
7 | %E | 与%e 相同 | 浮点类型 |
8 | %g | 根据实际情况判断输出%f 或者%e ,会去掉多余的0 | 浮点类型 |
9 | %b | 输出整型的二进制表现形式 | 数字类型 |
10 | %#b | 输出二进制完整的表现形式 | 数字类型 |
11 | %o | 输出整型的八进制表示 | 整型 |
12 | %#o | 输出整型的完整八进制表示 | 整型 |
13 | %x | 输出整型的小写十六进制表示 | 数字类型 |
14 | %#x | 输出整型的完整小写十六进制表示 | 数字类型 |
15 | %X | 输出整型的大写十六进制表示 | 数字类型 |
16 | %#X | 输出整型的完整大写十六进制表示 | 数字类型 |
17 | %v | 输出值原本的形式,多用于数据结构的输出 | 任意类型 |
18 | %+v | 输出结构体时将加上字段名 | 任意类型 |
19 | %#v | 输出完整Go语法格式的值 | 任意类型 |
20 | %t | 输出布尔值 | 布尔类型 |
21 | %T | 输出值对应的Go语言类型值 | 任意类型 |
22 | %c | 输出Unicode码对应的字符 | int32 |
23 | %U | 输出字符对应的Unicode码 | rune ,byte |
24 | %p | 输出指针所指向的地址 | 指针类型 |
使用 fmt.Sprintf 或者 fmt.Printf 来格式化字符串或者输出格式化字符串。
使用其它进制时,在 % 与格式化动词之间加上一个空格便可以达到分隔符的效果。
func main() {
str := "abcdefg"
fmt.Printf("%x\n", str)
fmt.Printf("% x\n", str)
}
输出的结果如下。
61626364656667
61 62 63 64 65 66 67
在使用数字时,还可以自动补零。
fmt.Printf("%09d", 1)
// 000000001
二进制同理。
fmt.Printf("%09b", 1<<3)
// 000001000
错误情况
格式化字符数量 < 参数列表数量
fmt.Printf("", "") //%!(EXTRA string=)
格式化字符数量 > 参数列表数量
fmt.Printf("%s%s", "") //%!s(MISSING)
类型不匹配
fmt.Printf("%s", 1) //%!s(int=1)
缺少格式化动词
fmt.Printf("%", 1) // %!(NOVERB)%!(EXTRA int=1)
输入
输入的话是通常使用 fmt 包下提供的三个函数。
// 扫描从os.Stdin读入的文本,根据空格分隔,换行也被当作空格
func Scan(a ...any) (n int, err error)
// 与Scan类似,但是遇到换行停止扫描
func Scanln(a ...any) (n int, err error)
// 根据格式化的字符串扫描
func Scanf(format string, a ...any) (n int, err error)
需要注意的是,Go中输入的默认分隔符号是空格。
func main() {
var s, s2 string
fmt.Scan(&s, &s2)
fmt.Println(s, s2)
}
// 输出
// a
// b
// a b
使用 fmt.Scanln。
func main() {
var s, s2 string
fmt.Scanln(&s, &s2)
fmt.Println(s, s2)
}
// 输出
// a b
// a b
使用 fmt.Scanf。
func main() {
var s, s2, s3 string
scanf, err := fmt.Scanf("%s %s \n %s", &s, &s2, &s3)
if err != nil {
fmt.Println(scanf, err)
}
fmt.Println(s)
fmt.Println(s2)
fmt.Println(s3)
}
// 输出
// aa bb
// cc
// aa
// bb
// cc
缓冲
当对性能有要求时可以使用 bufio 包进行读取。
func main() {
// 读
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
fmt.Println(scanner.Text())
}
// 输出
// abcedfg
// abcedfg
func main() {
// 写
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("hello world!\n")
writer.Flush()
fmt.Println(writer.Buffered())
}
// 输出
// hello world!
// 0
指针
不像 Java 和 .NET,Go 语言为程序员提供了控制数据结构的指针的能力。但是,我们不能进行指针运算。
通过给程序员基本内存布局,Go 语言允许我们控制特定集合的数据结构、分配的数量以及内存访问模式,这些对构建运行良好的系统是非常重要的。
另一方面,指针对于性能的影响是不言而喻的,而如果想要做的是系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。
程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,比如0x6b0820 或 0xf84001d7f0。
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
// 下面的代码片段可能输出 A: 5,address: 0x6b0820(这个值随着你每次运行程序而变化)
var i = 3
fmt.Printf("A: %d, address: %p\n", i, &i)
// 这个地址可以存储在一个叫做指针的特殊数据类型中,在这是一个指向 int 的指针,即 i,此处使用 *int 表示。如果我们想调用指针 intP,我们可以这样声明它
var intP *int
// 然后使用 intP = &i 是合法的,此时 intP 指向 i(指针的格式化标识符为 %p)
// intP 存储了 i1 的内存地址;它指向了 i1 的位置,它引用了变量 i1
一个指针变量可以指向任何一个值的内存地址,它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。
当然,可以声明指针指向任何类型的值来表明它的原始性或结构性。我们可以在指针类型前面加上 * 号(前缀)来获取指针所指向的内容,这里的 * 号是一个类型更改器。
使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
一个指针变量通常缩写为 ptr。
注意事项
在书写表达式类似 var p *type 时,切记在 * 号和指针名称间留有一个空格,因为 var p*type 是语法正确的,但是在更复杂的表达式中,它容易被误认为是一个乘法表达式。
符号 * 可以放在一个指针前,如 *intP,那么它将得到这个指针指向地址上所存储的值,这被称为反引用(或者内容或者间接引用)操作符,另一种说法是指针转移。
对于任何一个变量 var, 如下表达式都是正确的,var == *(&var)。
package main
import "fmt"
func main() {
var i1 = 5
fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1)
var intP *int
intP = &i1
fmt.Printf("The value at memory location %p is %d\n", intP, *intP)
}
// 输出
// An integer: 5, its location in memory: 0x24f0820
// The value at memory location 0x24f0820 is 5
package main
import "fmt"
func main() {
s := "good bye"
var p *string = &s
*p = "ciao"
fmt.Printf("Here is the pointer p: %p\n", p) // prints address
fmt.Printf("Here is the string *p: %s\n", *p) // prints string
fmt.Printf("Here is the string s: %s\n", s) // prints same string
}
// 输出
// Here is the pointer p: 0x2540820
// Here is the string *p: ciao
// Here is the string s: ciao
// 通过对 *p 赋另一个值来更改 “对象”,这样 s 也会随之更改
注意事项
我们不能得到一个文字或常量的地址。
const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10
所以,Go 语言和 C、C++ 以及 D 语言这些低层(系统)语言一样,都有指针的概念。但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。
因此 c = *p++ 在 Go 语言的代码中是不合法的。
指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
另一方面(可能低),由于一个指针导致的间接引用(一个进程执行了另一个地址),指针的过度频繁使用也会导致性能下降。
指针也可以指向另一个指针,并可以进行任意深度的嵌套,使你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。
如我们所见,在大多数情况下 Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,比如自动反向引用。
对一个空指针的反向引用是不合法的,并且会使程序崩溃。
package main
func main() {
var p *int = nil
*p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference
Go 保留了指针,在一定程度上保证了性能,同时为了更好的 GC 和安全考虑,又限制了指针的使用。
创建
关于指针有两个常用的操作符,一个是取地址符 &,另一个是解引用符 * 。对一个变量进行取地址,会返回对应类型的指针。
func main() {
a := 2
p := &
fmt.Println(p)
}
// 指针存储的是变量 a 的地址
// 输出 0xc00001c088
解引用符则有两个用途,第一个是访问指针所指向的元素,也就是解引用。
func main() {
num := 2
p := &num
rawNum := *p
fmt.Println(rawNum)
}
p 是一个指针,对指针类型解引用就能访问到指针所指向的元素。还有一个用途就是声明一个指针。
func main() {
var numPtr *int
fmt.Println(numPtr)
}
// 输出 <nil>
*int 代表该变量的类型是一个 int
类型的指针,不过指针不能光声明,还得初始化,需要为其分配内存,否则就是一个空指针,无法正常使用。要么使用取地址符将其他变量的地址赋值给该指针,要么就使用内置函数 new 手动分配。
func main() {
var numPtr *int
numPtr = new(int)
fmt.Println(numPtr)
}
更多的时候使用短变量。
func main() {
numPtr := new(int)
fmt.Println(numPtr)
}
new函数 只有一个参数那就是类型,并返回一个对应类型的指针,函数会为该指针分配内存,并且指针指向对应类型的零值。
func main() {
fmt.Println(*new(string))
fmt.Println(*new(int))
fmt.Println(*new([5]int))
fmt.Println(*new([]float64))
}
// 输出
//
// 0
// [0 0 0 0 0]
// []
禁止指针运算
在 Go 中是不支持指针运算的,也就是说指针无法偏移,先看下面的一段C++代码。
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = &arr[0];
cout << &arr << endl
<< p << endl
<< p + 1 << endl
<< &arr[1] << endl;
}
// 输出
// 0x31d99ff880
// 0x31d99ff880
// 0x31d99ff884
// 0x31d99ff884
可以看出数组的地址与数字第一个元素的地址一致,并且对指针加一运算后,其指向的元素为数组第二个元素。
Go中的数组也是如此,不过区别在于指针无法偏移。
func main() {
arr := [5]int{0, 1, 2, 3, 4}
p := &arr
println(&arr[0])
println(p)
// 试图进行指针运算
p++
fmt.Println(p)
}
这样的程序将无法通过编译,报错如下。
main.go:10:2: invalid operation: p++ (non-numeric type *[5]int)
提示
标准库 unsafe 提供了许多用于低级编程的操作,其中就包括指针运算。
new和make
在前面的几节已经很多次提到过内置函数 new和 make,两者有点类似,但也有不同。
func new(Type) *Type
这里返回值是类型指针;接收参数是类型;专用于给指针分配内存空间。
func make(t Type, size ...IntegerType) Type
这里返回值是值,不是指针;接收的第一个参数是类型,不定长参数根据传入类型的不同而不同;专用于给切片,映射表,通道分配内存。
new(int) // int指针
new(string) // string指针
new([]int) // 整型切片指针
make([]int, 10, 100) // 长度为10,容量100的整型切片
make(map[string]int, 10) // 容量为10的映射表
make(chan int, 10) // 缓冲区大小为10的通道
时间和日期
time 包为我们提供了一个数据类型 time.Time(作为值使用)以及显示和测量时间和日期的功能函数。
当前时间可以使用 time.Now() 获取,或者使用 t.Day()、t.Minute() 等等来获取时间的一部分,我们甚至可以自定义时间格式化字符串。
fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year())
// 输出 9.08.2024(这里就是当前系统时间)
Duration 类型表示两个连续时刻所相差的纳秒数,类型为 int64。
Location 类型映射某个时区的时间,UTC 表示通用协调世界时间。
包中的一个预定义函数 func (t Time) Format(layout string) string 可以根据一个格式化字符串来将一个时间 t 转换为相应格式的字符串,我们可以使用一些预定义的格式,比如 time.ANSIC 。
如果我们需要在应用程序在经过一定时间或周期执行某项任务(事件处理的特例),则可以使用 time.After 或者 time.Ticker。另外,time.Sleep(Duration d) 可以实现对某个进程(实质上是 goroutine)时长为 d 的暂停。