**本人是第六届字节跳动青训营(后端组)的成员。本文由博主本人整理自该营的日常学习实践,首发于稀土掘金:🔗Go语言入门指南:基础语法和常用特性解析 | 青训营
本文主要梳理自第六届字节跳动青训营(后端组)-Go语言原理与实践第一节(王克纯老师主讲)。同时博主也结合了自己的理解和其他参考资料,对Go语言基础语法和常用特性进行解析。后续若有需要补充的地方,本文会相应地进行更新。
如何安装和配置Go的开发环境,这里就不细说了,有需要的朋友可以自己去搜索教程。
我的go开发环境:
*本地IDE:GoLand 2023.1.2
*go:1.20.6
其它参考资料:
《Go语言圣经》
面向加薪学习-欢喜哥
Go语言官方文档
一、Hello World程序
1、在GoLand中运行一个Hello World
首先,新建一个go的项目:
写下如下代码:
没错,Go语言中的Hello World程序长这样:
package main // 声明 main 包import "fmt" // 导入 fmt 包,打印字符串时需要用到func main(){ // 声明 main 主函数fmt.Println("Hello, World!") // 打印
}
Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(静态编译)。Go语言提供的工具都通过一个单独的命令go
调用,go
命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。
在终端输入
go run main.go
即可编译并且运行这个go文件,go build main.go
即可编译而不运行这个文件。
在GoLand中,直接右键,点击运行(或点击func main左边的绿色小箭头),就能运行出第一个Go语言程序了:
Go语言原生支持Unicode,它可以处理全世界任何语言的文本。
接下来我们来讨论一下这个Hello World程序本身。
2、package包
声明包
Go语言以包(package)为管理单位,包类似于其它语言里的库(libraries)或者模块(modules)。
每个 Go 源文件必须以一条package
声明语句,先声明它所属的包。比如上面Hello world程序中的package main
,它表示该文件属于哪个包。一般来说,程序的包名由目录名来指定:
不过,一个go文件声明的包名也可以与其目录名不一致,但同一个目录下的go文件声明的包名必须一致:
main包与main函数
main
包比较特殊。它定义了一个独立可执行的程序,而不是一个库。
在main
包里的main
函数也很特殊,它是整个程序执行时的入口(C系语言差不多都这样,比如Java,C#)。main
函数所做的事情就是程序所做的。当然,main
函数一般调用其它包里的函数完成很多工作(如:fmt.Println
)。
也就是说,main包是一个go语言程序的入口包,func main()是go语言的入口函数。一个go语言项目程序中:
- 必须有且仅有一个main包;
- main 函数只能声明在 main 包中,不能声明在其他包中;
- 一个 main 包中也必须有且仅有一个 main 函数。
如果一个程序没有main包或main包中没有main函数,那么它在编译的时候将会出错。无法生成执行文件。
如果执意运行,GoLand会弹出下面的界面,提示没有入口包main(若用命令行执行则会报错):
从main函数的声明可以得知,在 Go 语言中,所有函数都以关键字 func 开头的,定义格式如下:
func 函数名 (参数列表) (返回值列表){函数体
}
总结go语言中的包
Go 语言的包与文件夹是一一对应的,它具有以下几点特性:
- 一个目录下的同级文件属于同一个包。
- 包名可以与其目录名不同。
- main 包是 Go 语言程序的入口包,一个 Go 语言程序必须有且仅有一个 main 包。如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。
3、import导入包
在包声明之后,是 import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""
包围,格式如下:
import "name" // 其中 import 是导入包的关键字,name 为所导入包的名字。
有一点需要注意:导入的包中不能含有代码中没有使用到的包,否则 Go 编译器会报编译错误,例如 imported and not used: "xxx"
,”xxx” 表示包名。如果在GoLand中出现了冗余的包,它会自动给你删除冗余包的导入。也可以使用一个 import 关键字导入多个包,此时需要用括号( )
将包的名字包围起来,并且每个包名占用一行,也就是写成下面的样子:
import("name1""name2"
)
二、程序结构
1、命名
Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母( Unicode 字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。 且大写字母和小写字母是不同的。
Go语言中关键字有25个,关键字不能用于自定义名字:
对于Go语言字段的命名,有以下注意点:
- 如果一个名字是在函数内部定义,那么它就只在函数内部有效。
- 如果是在函数外部定义,那么将在当前包的所有文件中都可以访问(相当于是全局变量)。
- 名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它是可以被外部的包访问的(相当于Java中的public),例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。
- 包本身的名字一般总是用小写字母。
此外,命名的长度没有逻辑上的限制,但是Go语言的编码风格是尽量使用短小的名字(尤其是局部变量)。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。
习惯上,Go语言的变量以 驼峰式 命名,而不是用下划线分隔。而像ASCII和HTML这样的缩略词,则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。
2、变量
王克纯讲师的给出的变量声明代码如下:
package mainimport ("fmt""math"
)func main() {var a = "initial" //声明string类型变量,省略变量类型var b, c int = 1, 2 //完整的声明var d = true //声明bool类型变量var e float64 //声明浮点类型变量f := float32(e) //短变量声明g := a + "foo" //用加号连接两个字符串fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0fmt.Println(g) // initialapple
}
非常清晰易懂。Go语言是一门强类型语言,每一个变量都有它自己的变量类型,常见的变量类型包括字符串,整数,浮点型,布尔型等。Go 语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。
在Go语言里面,大部分运算符的使用和优先级都和 C 或者 C++ 类似,这里就不再概述。
下面梳理了一下Go中变量的声明几种规则。
声明变量的一般形式是使用 var 关键字。
var 变量名字 类型 = 表达式var num int = 10
其中,“变量的类型”
或“=表达式
”
这两个部分可以省略其中的一个。也就是说,也可以这样写:
var num1 = 10 //省略类型
var num2 int //省略初始化表达式
-
如果省略的是类型信息,那么将根据等号右边的初始化表达式来推导该变量是什么类型。
-
如果初始化表达式被省略,那么将用零值初始化该变量。
- 数值类型变量对应的零值是0
- 布尔类型变量对应的零值是false
- 字符串类型对应的零值是空字符串
- 接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil(意义相当于其它语言的null)
- 数组或结构体等聚合类型对应的零值是每个元素或字段都对应的该类型的零值
零值初始化机制可以确保每个声明的变量总是有一个值,因此在Go语言中不存在未初始化的变量。
接下来来讲一讲如何声明变量。
方法一:声明一个变量但不赋值(采用默认值)
package mainimport "fmt"func main(){// 方法一:声明一个变量, 默认的值是0var a intfmt.Println("a = ", a)fmt.Printf("a的类型是: %T\n", a)// %T 是一种格式化占位符,用于在 fmt.Printf 或 fmt.Sprintf 等函数中,//将变量的类型信息格式化并插入到字符串中。
}/*输出:a = 0
a的类型是: int*/
方法二:声明一个变量,并初始化一个值
package mainimport "fmt"func main(){// 方法二:声明一个变量, 初始化一个值var num1 int = 100fmt.Printf("num1 = %d, type of num1 = %T\n", num1, num1)var num2 string = "hello"fmt.Printf("num2 = %s, num2的类型是: %T\n", num2, num2)
}
方法三:在初始化时省去数据类型,自动推导当前数据类型
package mainimport "fmt"func main(){// 方法三:在初始化时省去数据类型,自动推导当前数据类型var num1 = 100fmt.Printf("num1 = %d, type of num1 = %T\n", num1, num1)var num2 = "hello"fmt.Printf("num2 = %s, num2的类型是: %T\n", num2, num2)
}
方法四:多变量声明
也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,可以声明多个类型不同的变量(每个变量的类型都由初始化表达式去推导):
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
这样,就可以用以下的方式来交换两个变量的值,这比C语言,Java更加简洁。
i, j = j, i // 交换 i 和 j 的值
方法五:短变量声明
在函数内部(只能在函数内),有一种简短变量声明语句的形式,可用于声明和初始化局部变量。
它以名字 := 表达式
形式声明变量,变量的类型根据表达式来自动推导。
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
因为短变量的声明方式更加简洁和灵活,它被广泛用于大部分的局部变量的声明和初始化。
var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量:
i, j := 0, 1
但是,《Go语言圣经》中指出,这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。
总而言之,需要记住,:=
是一个变量声明语句,而=
是一个变量赋值操作。
和普通var形式的变量声明语句一样,短变量声明语句也可以用函数的返回值来声明和初始化变量:
f, err := os.Open(name) //声明变量f,err,并接收函数的返回值来初始化它们
if err != nil {return err
}
// ...use f...
f.Close()
短变量声明的注意事项
短变量声明中,左边的变量可能并不是全部都是刚刚声明的。如果有一些变量在与当前语句相同的作用域中已经声明过了,那么短变量声明语句对这些已经声明过的变量就只有赋值行为了。(如果是在外部的作用域中声明的,那依然会在短变量声明时重新创建一个新的变量。)
例如,在下面的代码中,两个语句的功能是不同的:
in, err := os.Open(infile) //声明了in和err两个变量
// ...
out, err := os.Create(outfile) //只声明了out一个变量,然后对已经声明的err进行了赋值操作
并且,简短变量声明语句中必须至少要声明一个新的变量:
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables//解决的方法是第二个简短变量声明语句改用普通的多重赋值语句
var f,err = os.Create(outfile)
3、常量
常量就是只读属性,定义后不允许被修改。定义一个常量,使用 const 关键字且定义的时候就要赋值。
package mainimport ("fmt""math"
)func main() {const s string = "constant" //声明常量const h = 500000000const i = 3e20 / hfmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
可以使用const来定义枚举类型:
package mainimport "fmt"// const来定义枚举类型
const (BEIJING = 0SHANGHAI = 1SHENZHEN = 2
)func main() {fmt.Println("BEIJING = ", BEIJING) // 0fmt.Println("SHANGHAI = ", SHANGHAI) // 1fmt.Println("SHENZHEN = ", SHENZHEN) // 2
}
4、if-else
package mainimport "fmt"func main() {if 7%2 == 0 {fmt.Println("7 is even")} else {fmt.Println("7 is odd")}if 8%4 == 0 {fmt.Println("8 is divisible by 4")}if num := 9; num < 0 {fmt.Println(num, "is negative")} else if num < 10 {fmt.Println(num, "has 1 digit")} else {fmt.Println(num, "has multiple digits")}
}
Go语言里面的 if-else 写法和 C 或者 C++ 类似。不同点一个是 if 后面不加括号。如果你写了括号,那么在保存的时候编译器会自动去掉。第二个不同点是 Golang 里面的 if 它必须后面接大括号,不能像 C 或者 C++ 一样直接把 if 后面的大括号另起一行写。
(比较有趣的一点是,当写错了,刷个新或者切屏一下再切回来,编译器就会很贴心地帮你把错误的书写方式更改过来了。)
5、for
package mainimport "fmt"func main() {i := 1//死循环for {fmt.Println("loop")break //使用break跳出循环}for j := 7; j < 9; j++ {fmt.Println(j)}for n := 0; n < 5; n++ {if n%2 == 0 {continue //使用continue跳过当前循环}fmt.Println(n)}for i <= 3 {fmt.Println(i)i = i + 1}
}
在Go里没有while,do while循环,只有一种for循环(毕竟一个for完全够用)。
- 最简单的for循环就是for后面啥也不写,这代表一个死循环。循环途中想要退出,使用break。
- 也可以使用经典的C中的循环,即
for(i := 0; i <= n; i++)
。这中间三段,任何一段都可以省略。 - 在循环里面,可以用 break 或者 continue 来跳出或者继续循环,这与C语言循环中的break和continue的用法是相似的。
6、switch
package mainimport ("fmt""time"
)func main() {a := 2switch a {case 1:fmt.Println("one")case 2:fmt.Println("two")case 3:fmt.Println("three")case 4, 5:fmt.Println("four or five")default:fmt.Println("other")}t := time.Now()switch {case t.Hour() < 12:fmt.Println("It's before noon")default:fmt.Println("It's after noon")}
}/*
输出:
two
It's after noon
*/
Go语言里面的 switch 分支结构与 C 或者 C++ 的不同点在于:
- 在Go语言中不需要显式地加break。执行完匹配的case代码块后,它会直接退出
switch-case
,如果没有任何一个匹配,会执行default
的代码块。Go不像C一样,如果不显式地加 break 就会继续往下跑完所有的 case。(正如上面的代码第一个输出的是two,而不是把case 2下面的打印语句全部执行一遍。 ) - 相比C或者C++,Go语言里面的switch-case功能更加强大。switch后可以使用任意的变量类型甚至函数,可以用来取代任意的 if-else 语句。
(1)一个case多个条件
在 Go 中,case
后可以接多个条件,多个条件之间是 或 的关系,用逗号 ,
相隔。
month := 5
switch month {
case 1, 3, 5, 7, 8, 10, 12:fmt.Println("该月份有 31 天")
case 4, 6, 9, 11:fmt.Println("该月份有 30 天")
case 2:fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
default:fmt.Println("输入有误!")
}
(2)选择语句高级写法
switch
还有另外一种写法,它包含一个 statement
可选语句部分,该可选语句在表达式之前运行:
switch month := 5; month {
case 1, 3, 5, 7, 8, 10, 12:fmt.Println("该月份有 31 天")
case 4, 6, 9, 11:fmt.Println("该月份有 30 天")
case 2:fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
default:fmt.Println("输入有误!")
}
这样,这里 month
变量的作用域就仅限于这个 switch
内了。
(3)fallthrough 语句
正常情况下 switch-case
语句在执行时只要有一个 case
满足条件,就会直接退出 switch-case
,如果一个都没有满足,才会执行 default
的代码块。不同于其他语言需要在每个 case
中添加 break
语句才能退出。
使用 fallthrough
语句可以在已经执行完成的 case
之后,向下“穿透”一层。
package mainimport ("fmt""time"
)func main() {a := 2switch a {case 1:fmt.Println("one")case 2:fmt.Println("two")fallthroughcase 3:fmt.Println("three")case 4, 5:fmt.Println("four or five")default:fmt.Println("other")}
}/*
输出:
two
three
*/
fallthrough
语句是 case
子句的最后一个语句。如果它出现在了 case
语句的中间,编译会不通过。
(4)无表达式的 switch
switch
后面的表达式可以省略,然后在case中写条件分支。此时switch-case的作用,相当于一个 if-elseif-else。
score := 88
switch {
case score >= 90 && score <= 100:fmt.Println("grade A")
case score >= 80 && score < 90:fmt.Println("grade B")
case score >= 70 && score < 80:fmt.Println("grade C")
case score >= 60 && score < 70:fmt.Println("grade D")
case score < 60:fmt.Println("grade E")
}
这比使用多个 if-else 逻辑更加清晰。
(5)switch 后可接函数
switch
后面可以接一个函数,只要保证 case
后的值类型与函数的返回值一致即可。
package mainimport "fmt"func getResult(args ...int) bool {for _, v := range args {if v < 60 {return false}}return true
}func main() {chinese := 88math := 90english := 95switch getResult(chinese, math, english) {case true:fmt.Println("考试通过")case false:fmt.Println("考试未通过")}
}
三、复合数据类型
基本数据类型是Go语言世界的原子,它包括整型int,浮点数float32,复数,布尔型bool,字符串string和常量const。
而复合数据类型是以不同的方式组合基本类型而构造出来的。主要有四种:数组、slice、map和结构体。
数组和结构体是聚合类型,它们的值由许多元素或成员字段的值组成。
数组是由同构的元素组成——每个数组元素都是完全相同的类型,结构体则是由异构的元素组成的。
数组和结构体都是有固定内存大小的数据结构,相比之下,slice和map则是动态的数据结构,它们可以根据需要动态增长。
1、数组
package mainimport "fmt"func main() {var a [5]int //一个可以存放 5 个int元素的数组 aa[4] = 100fmt.Println("get:", a[2])fmt.Println("len:", len(a))b := [5]int{1, 2, 3, 4, 5}fmt.Println(b)var twoD [2][3]intfor i := 0; i < 2; i++ {for j := 0; j < 3; j++ {twoD[i][j] = i + j}}fmt.Println("2d: ", twoD)
}
数组就是一个具有编号且长度固定的元素序列。对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。不过,在真实业务代码里面很少直接使用数组,因为它长度是固定的,用的更多的是切片。
数组遍历
使用 for range
循环可以获取数组每个索引以及索引上对应的元素:
func showArr() {arr := [...]string{"Go123", "Go456", "Go789"}for index, value := range arr {fmt.Printf("arr[%d]=%s\n", index, value)}for _, value := range arr {fmt.Printf("value=%s\n", value)}
}
输出结果:
注意,Go 中的数组是值类型而不是引用类型。当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,不会影响原始数组。
func arrByValue() {arr := [...]string{"Go123", "Go456", "Go789"}copy := arrcopy[0] = "Golang"fmt.Println(arr)fmt.Println(copy)
}
输出结果:
2、slice
切片不同于数组,可以任意更改长度,也有更丰富的操作。
package mainimport "fmt"func main() {s := make([]string, 3)s[0] = "a"s[1] = "b"s[2] = "c"fmt.Println("get:", s[2]) // cfmt.Println("len:", len(s)) // 3s = append(s, "d")s = append(s, "e", "f")fmt.Println(s) // [a b c d e f]c := make([]string, len(s))copy(c, s)fmt.Println(c) // [a b c d e f]fmt.Println(s[2:5]) // [c d e]fmt.Println(s[:5]) // [a b c d e]fmt.Println(s[2:]) // [c d e f]good := []string{"g", "o", "o", "d"}fmt.Println(good) // [g o o d]
}
切片是对数组的一个连续片段的引用,切片是一个引用类型。
切片本身不拥有任何数据,它们只是对现有数组的引用,每个切片值都会将数组作为其底层的数据结构。
slice 的语法和数组很像,只是没有固定长度而已。
创建切片
a.使用 []Type
可以创建一个带有 Type
类型元素的切片
// 声明整型切片
var numList []int //未赋值,numList默认值是nil// 声明一个空切片
var numListEmpty = []int{}
b.使用 make
函数构造一个切片,格式为 make([]Type, size, cap)
package mainimport ("fmt"
)func main() {// 创建一个初始长度为 3,容量为 5 的整数切片slice := make([]int, 3, 5)fmt.Println("切片长度:", len(slice))fmt.Println("切片容量:", cap(slice))
}
Type
:表示切片的元素类型。size
:表示切片的长度(包含的元素数量)。cap
:表示切片的容量(capability,底层数组的长度,即可以容纳的元素数量上限)。
c.通过对数组进行片段截取创建一个切片
arr := [5]string{"Go123", "Go456", "Go789", "Go1101112", "Go131415"}
var s1 = arr[1:4] //左闭右开
fmt.Println(arr)
fmt.Println(s1)
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。
有点类似于Python,但不同于Python,Go不支持负数索引。
切片的长度和容量
一个 slice 由三个部分构成:指针、长度和容量。
指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。
长度对应 slice 中元素的数目;长度不能超过容量。
容量一般是从 slice 的开始位置到底层数据的结尾位置。
简单的讲,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数。
如果切片操作超出上限将导致一个 panic
异常。
s := make([]int, 3, 5)
fmt.Println(s[10]) //panic: runtime error: index out of range [10] with length 3
切片元素的修改
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
使用 append
可以将新元素追加到切片上。append
函数的定义是 func append(slice []Type, elems ...Type) []Type
。其中 elems ...Type
在函数定义中表示该函数接受参数 elems
的个数是可变的。这些类型的函数被称为可变函数。
当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。
3、数组与切片的区别
数组(Array)
- 固定长度: 数组是一种固定长度的数据结构,定义数组时需要指定其长度,并且长度在创建后不能改变。
- 值类型 : 数组是值类型,当将数组赋值给另一个数组时,会复制数组的内容。
- 内存 分配: 数组的内存是一次性分配的,所以它们在内存中占据一块连续的存储空间。
- 声明和初始化: 数组的声明和初始化可以使用大括号
{}
,也可以在声明时指定元素的值。
// 声明一个包含 5 个整数的数组
var arr [5]int //必须显式指定长度,不能省略
arr := [5]int{1, 2, 3, 4, 5}
切片(Slice)
- 可变长度: 切片是动态长度的数据结构,可以根据需要进行扩容或缩减。
- 引用类型: 切片是引用类型,复制切片时只会复制一个引用,而不是整个数据内容。
- 内存 分配: 切片的底层是由数组支持的,底层数组的长度可能大于切片的长度。
- 声明和初始化: 切片的声明和初始化使用
make
函数,或者通过从现有数组或切片中切取子集来创建。
// 使用 make 函数创建一个包含 3 个整数的切片
slice := make([]int, 3)
// 从现有数组或切片中切取子集创建切片
subSlice := arr[1:3] // 包含索引 1 和 2 的元素
// 直接定义一个切片 不用指定长度,切片会根据元素个数自动确定长度
nums := []int{1, 2, 3, 4, 5}
总结来说,数组和切片都用于存储一组相同类型的数据,但数组具有固定长度和值类型特点,而切片具有可变长度和引用类型特点。通常情况下,切片更加灵活,因为它们支持动态大小调整。
4、map
map 是实际使用过程中最频繁用到的数据结构。在其它语言里叫做字典或者哈希。
package mainimport "fmt"func main() {m := make(map[string]int) //key的类型是string,value的类型是intm["one"] = 1m["two"] = 2fmt.Println(m) // map[one:1 two:2]fmt.Println(len(m)) // 2fmt.Println(m["one"]) // 1fmt.Println(m["unknow"]) // 0r, ok := m["unknow"]fmt.Println(r, ok) // 0 falsedelete(m, "one")m2 := map[string]int{"one": 1, "two": 2}var m3 = map[string]int{"one": 1, "two": 2}fmt.Println(m2, m3)
}
我们可以用 make 来创建一个空 map。这里需要两个类型,第一个是 key 的类型,这里是 string,另一个是 value 的类型,这里是int。我们可以从里面去存储或者取出键值对。可以用 delete 从里面删除键值对。golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。
map是引用类型的,当 map
被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
可以在声明的时候直接对map进行初始化:
m := map[int]string{1: "Go123",2: "Go456",3: "Go789",
}
fmt.Println(m)
也可以只声明但不初始化,后续通过添加操作将元素添加进map。
map 操作
a.添加元素
// 使用 `map[key] = value` 向 map 添加元素。
m[4] = "Go101112"
b.更新元素
// 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
m[4] = "GoGoGo"
c.获取元素
// 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
fmt.Println(m[4])
d.删除元素
//使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete也不会报错。
delete(m, 4)
e.判断 key 是否存在
// 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
v3, ok := m[3]
fmt.Println(ok)
fmt.Println(v3)v5, ok := m[5]
fmt.Println(ok)
fmt.Println(v5)
map
的下标读取可以返回两个值,第一个值为当前 key
的 value
值,第二个值表示对应的 key
是否存在,若存在 ok
为 true
,若不存在,则 ok
为 false
。
f.遍历 map
// 遍历 map 中所有的元素需要用 for range 循环。
for key, value := range m {fmt.Printf("key: %s, value: %s\n", key, value)
}
g.获取 map 长度
// 使用 len 函数可以获取 map 长度
fmt.Println(len(m)) // 4
四、range关键字
range
是一个关键字,用于迭代数组、切片、映射、通道或字符串中的元素。
range
的使用方式取决于所遍历的数据类型。对于一个 slice 或者一个 map,可以用 range 来快速遍历,这样代码能够更加简洁。
比如 range 遍历数组或slice的时候,会返回两个值,第一个是索引,第二个是对应位置的值;遍历map也会返回key和value两个值。如果不需要索引,可以用下划线来忽略。
package mainimport "fmt"func main() {nums := []int{2, 3, 4}sum := 0for i, num := range nums {sum += numif num == 2 {fmt.Println("index:", i, "num:", num) // index: 0 num: 2}}fmt.Println(sum) // 9m := map[string]string{"a": "A", "b": "B"}for k, v := range m {fmt.Println(k, v) // b 8; a A}for k := range m {fmt.Println("key", k) // key a; key b}
}
以下是 range
在不同数据类型中的使用示例:
1. 数组和切片
nums := []int{2, 3, 4}// 使用 range 遍历切片
for index, value := range nums {fmt.Printf("Index: %d, Value: %d\n", index, value)
}
2. Map
person := map[string]int{"Alice": 25, "Bob": 30}// 使用 range 遍历映射
for key, value := range person {fmt.Printf("Name: %s, Age: %d\n", key, value)
}
3. Channel
ch := make(chan int)// 使用 range 遍历通道,等待通道关闭
go func() {for num := range ch {fmt.Println("Received:", num)}
}()ch <- 1
ch <- 2
close(ch) // 关闭通道
4. 字符串
在遍历字符串时,value
会表示当前字符的 Unicode 码点值(即“char”)。
text := "Hello, Go!"// 使用 range 遍历字符串
for index, char := range text {fmt.Printf("Index: %d, Char: %c\n", index, char)
}
输出:
在这些示例中,range
的语法是相同的:
for index, value := range collection
其中,index 是当前迭代的索引(或键),value 是当前元素的值。
需要注意的是,在使用 range
迭代切片、数组、映射和通道时,会为每个迭代创建一个新的变量副本,而不是直接访问原始数据。这对于遍历数据结构并进行操作是很有用的。
五、函数
这个是 Golang 里面一个简单的实现两个变量相加的函数。 Golang 和其他很多语言不一样的是,变量类型是后置的。
package mainimport "fmt"func add(a int, b int) int {return a + b
}func add2(a, b int) int {return a + b
}func exists(m map[string]string, k string) (v string, ok bool) {v, ok = m[k]return v, ok
}func main() {res := add(1, 2)fmt.Println(res) // 3v, ok := exists(map[string]string{"a": "A"}, "a")fmt.Println(v, ok) // A True
}
Golang 里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息。
六、指针
Go里面也支持指针。但是,相比 C 和 C++ 里面的指针,支持的操作很有限。指针的一个主要用途就是对于传入参数进行修改。
package mainimport "fmt"func add2(n int) {n += 2
}func add2ptr(n *int) {*n += 2
}func main() {n := 5add2(n)fmt.Println(n) // 5add2ptr(&n)fmt.Println(n) // 7
}package mainimport "fmt"type user struct {name stringpassword string
}func main() {a := user{name: "wang", password: "1024"}b := user{"wang", "1024"}c := user{name: "wang"}c.password = "1024"var d userd.name = "wang"d.password = "1024"fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}fmt.Println(checkPassword(a, "haha")) // falsefmt.Println(checkPassword2(&a, "haha")) // false
}func checkPassword(u user, password string) bool {return u.password == password
}func checkPassword2(u *user, password string) bool {return u.password == password
}
这个函数试图把一个变量+2。但是单纯像上面add2()这种写法其实是无效的,因为传入函数的参数实际上是一个拷贝。add2()中的这个n+=2,是对原变量n的拷贝进行了+2,回到main()中并不起作用。如果想要在函数中对外部变量的修改起作用的话,那么我们需要把那个类型写成指针类型。
为了类型匹配,调用的时候会加一个 & 符号。
七、结构体
结构体是带类型的字段的集合。
package mainimport "fmt"type user struct {name stringpassword string
}func main() {//可以用结构体的名称去初始化一个结构体变量,构造的时候需要传入每个字段的初始值a := user{name: "wang", password: "1024"}b := user{"wang", "1024"}c := user{name: "wang"}c.password = "1024"//也可以用这种键值对的方式去指定初始值,这样可以只对一部分字段进行初始化var d userd.name = "wang"d.password = "1024"fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}fmt.Println(checkPassword(a, "haha")) // falsefmt.Println(checkPassword2(&a, "haha")) // false
}func checkPassword(u user, password string) bool {return u.password == password
}func checkPassword2(u *user, password string) bool {return u.password == password
}
比如这里 user 结构体包含了两个字段,name 和 password。
同样的,结构体也能支持指针,这样能够实现对于结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。
结构体方法
在 Golang 里,可以为结构体定义一些方法。
结构体方法是与特定类型的结构体相关联的函数。结构体方法允许为结构体类型定义“附加的功能”,并且可以通过结构体实例调用这些方法。这在面向对象编程中类似于类的方法。
要定义一个结构体方法,需要:
- 先定义一个结构体类型。
- 然后,为该结构体类型定义一个方法。
比如将上面例子中的 checkPassword()从一个普通函数改成结构体方法。这样用户可以通过 a.checkPassword(“xx”) 这样去调用。
具体的代码修改,就是把第一个参数,加上括号,写到函数名称前面。在实现结构体的方法的时候也有两种写法,一种是带指针,一种是不带指针。它们的区别是,如果带指针的话,那么就可以对这个结构体去做修改。如果不带指针的话,那实际上操作的是一个拷贝,就无法对结构体进行修改。
package mainimport "fmt"type user struct {name stringpassword string
}func (u user) checkPassword(password string) bool {return u.password == password
}func (u *user) resetPassword(password string) {u.password = password
}func main() {a := user{name: "wang", password: "1024"}a.resetPassword("2048")fmt.Println(a.checkPassword("2048")) // true
}
以下例子演示了如何在 Go 中定义和使用结构体方法:
package mainimport ("fmt"
)// 定义一个结构体类型
type Rectangle struct {Width float64Height float64
}// 为 Rectangle 结构体定义一个方法
func (r Rectangle) Area() float64 {return r.Width * r.Height
}func main() {// 创建一个 Rectangle 结构体实例rect := Rectangle{Width: 10, Height: 5}// 调用结构体方法area := rect.Area()fmt.Println("矩形的面积:", area)
}
在上述示例中,我们定义了一个名为 Rectangle
的结构体类型,具有 Width
和 Height
两个字段。然后,我们为 Rectangle
结构体定义了一个名为 Area
的方法,用于计算矩形的面积。
结构体方法的语法如下:
func (receiver Type) MethodName() ReturnType {// 方法实现
}
receiver
:是方法的接收器,它定义了哪个结构体类型可以调用该方法。在上面的例子中,receiver
是Rectangle
结构体类型。MethodName
:是为该结构体定义的方法的名称。ReturnType
:是该方法返回的数据类型。
结构体方法在 Go 中被广泛使用,用于将操作与数据结构关联起来,提高代码的可读性和封装性。通过使用方法,可以将特定类型的功能封装到结构体中,并通过结构体实例调用这些方法来执行相关操作。
八、错误处理
在 Go 语言里,符合语言习惯的做法是使用一个单独的返回值来传递错误信息。
不同于 Java 使用的异常,Go语言的处理方式能够很清晰地知道是哪个函数返回了错误,并且能用简单的 if else 来处理错误。
在函数定义时,可以在函数的返回值类型列表后加一个 error, 代表这个函数可能会返回错误。那么在函数实现的时候, return 就需要同时 return 两个值:如果出现错误,那么 return nil 和一个 error;如果没有错误,那么返回原本的结果和 nil。
package mainimport ("errors""fmt"
)type user struct {name stringpassword string
}func findUser(users []user, name string) (v *user, err error) {for _, u := range users {if u.name == name {return &u, nil}}return nil, errors.New("not found")
}func main() {u, err := findUser([]user{{"wang", "1024"}}, "wang")if err != nil {fmt.Println(err)return}fmt.Println(u.name) // wangif u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {fmt.Println(err) // not foundreturn} else {fmt.Println(u.name)}
}
九、字符串操作
在标准库 strings 包里有很多常用的字符串工具函数,比如contains判断一个字符串里是否包含有另一个字符串 , count 字符串计数, index 查找某个字符串的位置。 join 连接多个字符串 repeat 重复多个字符串 replace 替换字符串。
package mainimport ("fmt""strings"
)func main() {a := "hello"fmt.Println(strings.Contains(a, "ll")) // truefmt.Println(strings.Count(a, "l")) // 2fmt.Println(strings.HasPrefix(a, "he")) // truefmt.Println(strings.HasSuffix(a, "llo")) // truefmt.Println(strings.Index(a, "ll")) // 2fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llofmt.Println(strings.Repeat(a, 2)) // hellohellofmt.Println(strings.Replace(a, "e", "E", -1)) // hEllofmt.Println(strings.Split("a-b-c", "-")) // [a b c]fmt.Println(strings.ToLower(a)) // hellofmt.Println(strings.ToUpper(a)) // HELLOfmt.Println(len(a)) // 5b := "你好"fmt.Println(len(b)) // 6
}
字符串的格式化
Go 语言中的格式化占位符和 C 语言中的格式化占位符在某些方面是相似的,它们用于将变量的值以特定格式插入到字符串中。以下是一些关于 Go 和 C 中格式化占位符的比较:
相似
%d
: 用于格式化整数(十进制)。%f
: 用于格式化浮点数。%s
: 用于格式化字符串。%c
: 用于格式化字符。%p
: 用于格式化指针地址。
不同
- 在 Go 语言中,没有像 C 语言中的
%i
一样的格式化占位符。在 Go 中,可以使用%d
来格式化整数(十进制)。 - Go 语言中的
%v
是一个通用的占位符,可以用于格式化任何类型的变量。它会根据变量的类型自动选择合适的格式。 - Go 语言中的
%T
用于格式化变量的类型。 - 在 C 语言中,一些特定于整数长度的占位符(如
%ld
、%lld
等)用于格式化长整型。而在 Go 中,可以使用%d
格式化不同长度的整数,Go 会自动处理。
在标准库的 fmt 包里面有很多的字符串格式相关的方法,可以很轻松地用 %v 来打印任意类型的变量,而不需要区分数字字符串。也可以用 %+v 打印详细结果,%#v 则更详细。
package mainimport "fmt"type point struct {x, y int
}func main() {s := "hello"n := 123p := point{1, 2}fmt.Println(s, n) // hello 123fmt.Println(p) // {1 2}fmt.Printf("s=%v\n", s) // s=hellofmt.Printf("n=%v\n", n) // n=123fmt.Printf("p=%v\n", p) // p={1 2}fmt.Printf("p=%+v\n", p) // p={x:1 y:2}fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}f := 3.141592653fmt.Println(f) // 3.141592653fmt.Printf("%.2f\n", f) // 3.14
}
十、JSON处理
在 Go 编程语言中,JSON(JavaScript Object Notation)处理是一项常见的任务,用于在应用程序之间传递和存储结构化的数据。Go 提供了内置的标准库来处理 JSON 数据,使得编码(将数据转换为 JSON 格式)和解码(将 JSON 数据转换为数据结构)变得非常容易。
对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写(也就是是公开字段),那么这个结构体就能用 json.Marshal
序列化,变成一个 JSON 的字符串。序列化之后的字符串也能够用 json.Unmarshal
反序列化到一个空的变量里。
以下是在 Go 中处理 JSON 数据的基本流程:
1. 编码( 序列化 ):将数据转换为 JSON 格式。
使用 encoding/json
包中的 Marshal
函数可以将 Go 数据结构编码为 JSON 格式的字节流。这通常用于在应用程序中将数据发送到其他系统或存储到文件中。
json.Marshal()
返回的是一个字节切片 []byte
和一个err,通过把字节切片转换成字符串,看到其中的内容。
package mainimport ("encoding/json""fmt"
)type Person struct {Name string `json:"name"`Age int `json:"age"`Email string `json:"email"`
}func main() {person := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}jsonBytes, err := json.Marshal(person)if err != nil {fmt.Println("JSON encoding error:", err)return}fmt.Println(string(jsonBytes)) //将字节切片转换为字符串,查看内容
}
输出:
可以看到,这样转换过来的json格式可读性较差,包含很长的字符串,并且没有空白缩进。为了生成便于阅读的格式,另一个json.MarshalIndent
函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:
data, err := json.MarshalIndent(person, "", " ")
if err != nil {log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
当需要把多个结构体(即一个结构体切片中的各个元素)都转换成json格式时,这个函数就非常有用了。
2. 解码(反 序列化 ):将 JSON 数据转换为 Go 数据结构。
使用 encoding/json
包中的 Unmarshal
函数可以将 JSON 格式的字节流解码为相应的 Go 数据结构。这通常用于从外部系统获取 JSON 数据并在应用程序中进行处理。
package mainimport ("encoding/json""fmt"
)type Person struct {Name string `json:"name"`Age int `json:"age"`Email string `json:"email"`
}func main() {jsonStr := `{"name":"Bob","age":25,"email":"bob@example.com"}`var person Personerr := json.Unmarshal([]byte(jsonStr), &person)if err != nil {fmt.Println("JSON decoding error:", err)return}fmt.Println("Name:", person.Name)fmt.Println("Age:", person.Age)fmt.Println("Email:", person.Email)
}
结构体字段的标签(json:"field"
)可以用于指定 JSON 键与 Go 结构体字段之间的映射关系。这在编码和解码过程中非常有用,确保正确的字段匹配。
补充代码示例:
package mainimport ("encoding/json""fmt"
)type userInfo struct {Name stringAge int `json:"age"`Hobby []string
}func main() {a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}buf, err := json.Marshal(a)if err != nil {panic(err)}fmt.Println(buf) // [123 34 78 97...] 字节切片fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}buf, err = json.MarshalIndent(a, "", "\t")if err != nil {panic(err)}fmt.Println(string(buf))var b userInfoerr = json.Unmarshal(buf, &b)if err != nil {panic(err)}fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
十一、时间处理
- 在Go语言里面最常用的就是
time.Now()
来获取当前时间。 - 也可以用
time.Date
构造一个带时区的时间。 t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()
有很多方法来获取这个时间点的年 月 日 小时 分钟 秒。- 用点
.Sub
对两个时间进行减法,得到一个时间段diff
。时间段diff
又可以得到它有多少小时,多少分钟、多少秒:diff.Minutes(), diff.Seconds()
- 在和某些系统交互的时候,我们经常会用到时间戳,可以用
.Unix
来获取时间戳。 t.Format()
可以将时间进行格式化。time.Parse()
用于将字符串解析为时间对象。它在处理时间格式的字符串时非常有用,可以将字符串转换为对应的time.Time
类型。
package mainimport ("fmt""time"
)func main() {now := time.Now()fmt.Println(now) // 2023-08-10 20:49:44.8566865 +0800 CST m=+0.007017101t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)fmt.Println(t) // 2022-03-27 01:25:36 +0000 UTCfmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36diff := t2.Sub(t) //t2和t之间的获得时间差fmt.Println(diff) // 1h5m0sfmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")if err != nil {panic(err)}fmt.Println(t3 == t) // truefmt.Println(now.Unix()) // 1648738080
}
十二、数字解析
Go语言中,关于字符串和数字类型之间的转换都在 strconv
这个包下。这个包是 string convert 这两个单词的缩写,可以用其中的 ParseInt() 或者 ParseFloat() 来解析一个字符串,把字符串按照要求转换为相应的整数。
我们还可以用该包中的 Atoi() 把一个十进制字符串转成数字,Atoi 是ASCII to Integer的缩写。反过来,可以用 Itoa() 把数字转成字符串。如果输入的参数不合法,那么这些函数都会返回error。
Atoi()和ParseInt()这两者的基本功能是相同的,都是将字符串解析为整数,但是 ParseInt
提供了更多的选项来处理不同的解析需求。例如,ParseInt
允许指定解析的进制(比如二进制、十六进制等),也可以指定结果的位数(比如 32 位整数或 64 位整数)。
package mainimport ("fmt""strconv"
)func main() {f, _ := strconv.ParseFloat("1.234", 64)fmt.Println(f) // 1.234n, _ := strconv.ParseInt("111", 10, 64)fmt.Println(n) // 111n, _ = strconv.ParseInt("0x1000", 0, 64)fmt.Println(n) // 4096n2, _ := strconv.Atoi("123")fmt.Println(n2) // 123n2, err := strconv.Atoi("AAA")fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}
十三、进程信息
- 在Go里我们能够用
os.Args
来得到程序执行时的指定的命令行参数。os.Args
是一个字符串切片,它包含了传递给程序的所有命令行参数,包括程序名称本身。比如我们编译的一个二进制文件,在command命令后接a b c d 来启动,那么os.Args
会是一个长度为 5 的 slice。第一个成员代表程序名称本身:/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main
,而命令行参数是a、b、c、d。 - os.Setenv用于读取环境变量的值。
exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
则用于在命令行执行grep
命令,查找文件/etc/hosts
中包含127.0.0.1
的行。exec.Command
创建了一个命令对象,.CombinedOutput()
方法运行该命令并捕获其标准输出和标准错误输出。如果命令执行出错,会抛出错误。
package mainimport ("fmt""os""os/exec"
)func main() {// go run example/20-env/main.go a b c dfmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...fmt.Println(os.Setenv("AA", "BB"))buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()if err != nil {panic(err)}fmt.Println(string(buf)) // 127.0.0.1 localhost
}
十二、Go语言特性
Go语言的特点有:
- 高性能、高并发。
- 语法简单、学习曲线平缓。
- 标准库丰富,工具链完善。很多情况下,不需要借助第三方库,就能完成大部分基础功能的开发。
- 静态链接。
- 快速编译,拥有静态语言中几乎最快的编译速度。
- 可跨平台。
- 垃圾回收。
Go语言的“语法简单”在很多地方都有体现,如以下HTTP服务器的Go语言代码案例:
Go语言代码实现HTTP服务器
package mainimport ("net/http"
)func main() {http.Handle("/", http.FileServer(http.Dir("."))) //路由处理http.ListenAndServe(":8080", nil) //服务器监听与启动
}
该代码只有10行,而核心的代码只有main函数中的两行。其中,http.Handle(“/”, http.FileServer(http.Dir(“.”))) 用于路由处理,使用 http.FileServer
函数创建一个路由处理器,并将其映射到根路径 (“/”)。http.FileServer
函数接受一个参数,即一个表示要提供的文件目录的 http.Dir
类型。
而 http.ListenAndServe(“:8080”, nil) 用于服务器的监听与启动,这行代码启动了一个 HTTP 服务器,监听本地主机的 8080 端口。http.ListenAndServe
函数接受两个参数,第一个参数是监听地址(在这里是 “:8080”),第二个参数是一个处理器(这里是 nil
,表示使用默认的路由处理器)。
这样一来,这段代码就创建了一个简单的本地服务器,它会将访问根路径的请求映射到当前程序所在的目录,并在本地的 8080 端口上启动一个服务器,允许用户通过浏览器或其他 HTTP 客户端访问这些文件。
我们可以在Go语言的开发环境GoLand中运行该代码片段,并随便导入一个html文件来进行测试:
运行go程序,启动服务器,访问localhost:8080/login.html,会发现,确实可以访问到该静态页面:
Go语言的基本语法入门就到这里。更为细致的知识点和用法,可以从Go语言的官方文档寻求帮助。