微软Go学习教程(上半部分)
0.下载Go
go version #查看go版本,是否安装成功.
go env #查看环境变量
1.Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
go run main.go
go build main.go
2.声明和使用变量
package main
import "fmt"
func main(){
var a string = "Harris" //变量 名称 类型 初始化
var aa,bb int = 12,13
var (
b = 12 //自动推断类型
c = "Jack"
)
age := 123 //最常用的声明和初始化方式
name := "Lisa"
fmt.Println(a)
fmt.Println(b)
fmt.Println(c,aa,bb)
fmt.Println(age,name)
}
Harris
12
Jack
需要记住的重要一点是,在 Go 中,当你声明一个变量但不使用它时,Go 会抛出错误,而不是像某些其他编程语言一样抛出警告。
声明常量
常量和变量之间既有相似之处,也有一些重要差异。 例如,你可以在不使用常量的情况下声明常量。 你不会收到错误消息。 不能使用冒号等于号来声明常量。 如果采用这种方式,Go 会发出警告。
const (
StatusOK = 0
StatusConnectionReset = 1
StatusOtherError = 2
)
3.数据类型
Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。
Go 有四类数据类型:
- 基本类型:数字、字符串和布尔值
- 聚合类型:数组和结构
- 引用类型:指针、切片、映射、函数和通道
- 接口类型:接口
整型数字
intint8int16int32int64intintuintuint8uint16uint32uint64
var integer8 int8 = 127
var integer16 int16 = 32767
var integer32 int32 = 2147483647
var integer64 int64 = 9223372036854775807
fmt.Println(integer8, integer16, integer32, integer64)
intintint32
运行该程序时,你会收到以下错误:
invalid operation: integer16 + integer32 (mismatched types int16 and int32)
如你所见,在 Go 中将值从一种类型转换为另一种类型时,需要显式声明新类型。 我们将在本模块结束时讨论如何正确地强制转换类型。
runeint32
rune := 'G'
fmt.Println(rune)
G71G
内置builtin.go 查看数据类型
https://go.dev/src/builtin/builtin.go
浮点数
var float32 float32 = 2147483647
var float64 float64 = 9223372036854775807
fmt.Println(float32, float64)
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.MaxFloat32, math.MaxFloat64)
}
布尔型
truefalsebool
因此,你可以按如下方式声明布尔变量:
var featureFlag bool = true
string"'
例如,下面的代码演示了声明和初始化字符串变量的两种方法:
package main
import (
"fmt"
)
func main() {
var firstName string = "John"
lastName := "Doe"
fmt.Println(firstName, lastName)
}
\
\n\r\t\'\"\\
默认值
到目前为止,几乎每次声明变量时,都使用值对其进行了初始化。 但与在其他编程语言中不同的是,在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。
下面列出了我们目前浏览过类型的几个默认值:
int0int64float32float64+0.000000e+000boolfalsestring
类型转换
在上一节中,我们确认在 Go 中隐式强制转换不起作用。 接下来,需要显式强制转换。 Go 提供了将一种数据类型转换为另一种数据类型的一些本机方法。 例如,一种方法是对每个类型使用内置函数,如下所示:
package main
import (
"fmt"
)
func main() {
var integer16 int16 = 127
var integer32 int32 = 32767
fmt.Println(int32(integer16) + integer32)
}
stringint
package main
import (
"fmt"
"strconv"
)
func main() {
i, _ := strconv.Atoi("-42")
s := strconv.Itoa(-42)
fmt.Println(i, s)
}
main函数
main()main()main()
main()main()os.Args
下面的代码从命令行读取两个数字,并为其求和:
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
number1, _ := strconv.Atoi(os.Args[1])
number2, _ := strconv.Atoi(os.Args[2])
fmt.Println("Sum:", number1+number2)
}
os.Argsstringint
若要运行程序,请使用以下命令:
go run main.go 3 5
4.自定义函数
func name(parameters) (results) {
body-content
}
func
若要练习此技巧,我们将重构上一节的代码,为自定义函数中的数字求和。 我们将使用以下代码:
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
sum := sum(os.Args[1], os.Args[2])
fmt.Println("Sum:", sum)
}
func sum(number1 string, number2 string) int {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
return int1 + int2
}
sum
func sum(number1 string, number2 string) (result int) {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
result = int1 + int2
return
}
returnreturn
返回多个值
func calc(number1 string, number2 string) (sum int, mul int) {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
sum = int1 + int2
mul = int1 * int2
return
}
__
package main
import "fmt"
func main() {
sum, _ := calc(os.Args[1], os.Args[2])
fmt.Println("Sum:", sum)
}
更改函数参数值
将值传递给函数时,该函数中的每个更改都不会影响调用方。 Go 是“按值传递”编程语言。 每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。 在函数中对该变量所做的更改都不会影响你向函数发送的更改。
例如,假设你创建了一个用于更新人员姓名的函数。 请注意,运行此代码时会发生的变化:
package main
import "fmt"
func main() {
firstName := "John"
updateName(firstName)
fmt.Println(firstName)
}
func updateName(name string) {
name = "David"
}
updateName
updateNamemainfirstName
在 Go 中,有两个运算符可用于处理指针:
&*
让我们修改前面的示例,以阐明指针的工作方式:
package main
import "fmt"
func main() {
firstName := "John"
updateName(&firstName)
fmt.Println(firstName)
}
func updateName(name *string) {
*name = "David"
}
string*string*updateName&
引用本地包
calculator包下初始化go.mod
go mod init github.com/myuser/calculator
文件目录结构
src/
calculator/
go.mod
sum.go
var_use/
main.go
var_use下初始化go.mod
go mod init var_use
module var_use
go 1.17
require github.com/myuser/calculator v0.0.0
replace github.com/myuser/calculator => ../calculator //本地路径下的位置
总结
我们介绍了开始在 Go 中构建更复杂应用程序所需的基础知识。 现在,你可以通过几种方法来声明和初始化变量。 你还了解 Go 提供的各种数据类型。 你已经使用最基本的数据类型,
并且还了解到如何创建函数来组织代码,并使代码更易于维护。 你已经了解到,Go 是“按值传递”语言,但它亦支持指针。 我们将在后续的部分模块中使用指针。
mainmain()
5.条件语句
package main
import "fmt"
func givemeanumber() int {
return -1
}
func main() {
if num := givemeanumber(); num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has only one digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
if
switch
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
sec := time.Now().Unix()
rand.Seed(sec)
i := rand.Int31n(3)
switch i {
case 0:
fmt.Print("zero...")
case 1:
fmt.Print("one...")
case 2:
fmt.Print("two...")
}
fmt.Println("ok")
}
如果多次运行前面的代码,则每次都会看到不同的输出。 (但是,如果在 Go Playground 中运行代码,则每次都会获得相同的结果。 这是此服务的局限性之一。)
switchnumnum5ok
也可让默认用例更加具体,像下面这样包含它:
switch i {
case 0:
fmt.Print("zero...")
case 1:
fmt.Print("one...")
case 2:
fmt.Print("two...")
default:
fmt.Print("no match...")
}
defaulticase
使用多个表达式
casecase,
以下代码示例演示了如何包含多个表达式。
package main
import "fmt"
func location(city string) (string, string) {
var region string
var continent string
switch city {
case "Delhi", "Hyderabad", "Mumbai", "Chennai", "Kochi":
region, continent = "India", "Asia"
case "Lafayette", "Louisville", "Boulder":
region, continent = "Colorado", "USA"
case "Irvine", "Los Angeles", "San Diego":
region, continent = "California", "USA"
default:
region, continent = "Unknown", "Unknown"
}
return region, continent
}
func main() {
region, continent := location("Irvine")
fmt.Printf("John works in %s, %s\n", region, continent)
}
调用函数
switchcasetime.Now()
package main
import (
"fmt"
"time"
)
func main() {
switch time.Now().Weekday().String() {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("It's time to learn some Go.")
default:
fmt.Println("It's weekend, time to rest!")
}
fmt.Println(time.Now().Weekday().String())
}
switch
case
package main
import "fmt"
import "regexp"
func main() {
var email = regexp.MustCompile(`^[^@]+@[^@.]+\.[^@.]+`)
var phone = regexp.MustCompile(`^[(]?[0-9][0-9][0-9][). \-]*[0-9][0-9][0-9][.\-]?[0-9][0-9][0-9][0-9]`)
contact := "foo@bar.com"
switch {
case email.MatchString(contact):
fmt.Println(contact, "is an email")
case phone.MatchString(contact):
fmt.Println(contact, "is a phone number")
default:
fmt.Println(contact, "is not recognized")
}
}
switch
switchiftrueswitch
switch
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().Unix())
r := rand.Float64()
switch {
case r > 0.1:
fmt.Println("Common case, 90% of the time")
default:
fmt.Println("10% of the time")
}
}
switchswitchifelse if
使逻辑进入到下一个 case
casebreakswitchfallthrough
若要更好地了解此模式,请查看以下代码示例。
package main
import (
"fmt"
)
func main() {
switch num := 15; {
case num < 50:
fmt.Printf("%d is less than 50\n", num)
fallthrough
case num > 100:
fmt.Printf("%d is greater than 100\n", num)
fallthrough
case num < 200:
fmt.Printf("%d is less than 200", num)
}
}
15 is less than 50
15 is greater than 100
15 is less than 200
numnumcasefallthroughcasefallthrough
for循环
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println("sum of 1..100 is", sum)
}
whilewhilefor
for
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var num int64
rand.Seed(time.Now().Unix())
for num != 5 {
num = rand.Int63n(15)
fmt.Println(num)
}
}
num5
break
for
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var num int32
sec := time.Now().Unix()
rand.Seed(sec)
for {
fmt.Print("Writting inside the loop...")
if num = rand.Int31n(10); num == 5 {
fmt.Println("finish!")
break
}
fmt.Println(num)
}
}
Continue 语句
continue
continue
package main
import "fmt"
func main() {
sum := 0
for num := 1; num <= 100; num++ {
if num%5 == 0 {
continue
}
sum += num
}
fmt.Println("The sum of 1 to 100, but excluding numbers divisible by 5, is", sum)
}
6.defer和panic和recover进行控制
deferdefer
可以根据需要推迟任意多个函数。 defer 语句按逆序运行,先运行最后一个,最后运行第一个。
通过运行以下示例代码来查看此模式的工作原理:
package main
import "fmt"
func main() {
for i := 1; i <= 4; i++ {
defer fmt.Println("deferred", -i)
fmt.Println("regular", i)
}
}
输出:
regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1
fmt.Println("deferred", -i)imain()regular
defer
package main
import (
"io"
"os"
"fmt"
)
func main() {
newfile, error := os.Create("learnGo.txt")
if error != nil {
fmt.Println("Error: Could not create file.")
return
}
defer newfile.Close()
if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
fmt.Println("Error: Could not write to file.")
return
}
newfile.Sync()
}
.Close()
panic函数
运行时错误会使 Go 程序崩溃,例如尝试通过使用超出范围的索引或取消引用 nil 指针来访问数组。 你也可以强制程序崩溃。
panic()panic
panic()
panicdefer
package main
import "fmt"
func highlow(high int, low int) {
if high < low {
fmt.Println("Panic!")
panic("highlow() low greater than high")
}
defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
fmt.Println("Call: highlow(", high, ",", low, ")")
highlow(high, low + 1)
}
func main() {
highlow(2, 0)
fmt.Println("Program finished successfully!")
}
Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high
goroutine 1 [running]:
main.highlow(0x866760, 0xc0000ce008)
D:/Go_Study/MS_Study/var_use/main.go:8 +0x285
main.highlow(0x2, 0x2)
D:/Go_Study/MS_Study/var_use/main.go:13 +0x20f
main.highlow(0x2, 0x1)
D:/Go_Study/MS_Study/var_use/main.go:13 +0x20f
main.highlow(0x2, 0x0)
D:/Go_Study/MS_Study/var_use/main.go:13 +0x20f
main.main()
D:/Go_Study/MS_Study/var_use/main.go:17 +0x25
Process finished with the exit code 2
下面是运行代码时会发生的情况:
highlow()lowhighPanic!Deferred...Program finished successfully!
panic()recover()
recover函数
func main() {
defer func() {
handler := recover()
if handler != nil {
fmt.Println("main(): recover", handler)
}
}()
highlow(2, 0)
fmt.Println("Program finished successfully!")
}
运行程序时,输出应该如下所示:
Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high
Program exited.
你是否看到了相对于上一版本的差异? 主要差异在于,你不再看到堆栈跟踪错误。
main()recover()recover()nil
panicrecovertry/catch
小练习
首先,编写一个用于输出数字(1 到 100)的程序,其中有以下变化:
FizzBuzzFizzBuzz
switch
package main
import "fmt"
func main(){
for i:=1;i<=100;i++ {
switch {
case i%3 == 0 && i%5 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}
7.数组和切片
在前面的模块中,我们介绍了 Go 中的基本数据类型,它们是在 Go 程序中构建数据结构的基础。 在本模块中,我们将介绍前面提到的聚合类型:数组和切片。 你将了解这两者之间的区别以及何时选择一种类型而弃用另一种。 你还将了解映射和结构,它们是使用 Go 操作数据的基础,并用于许多 Web 服务。
最后,你将学习如何构建自定义和复杂的数据类型,它们在分析 JSON 有效负载时将非常有用。
使用数组
Go 中的数组是一种特定类型且长度固定的数据结构。 它们可具有零个或多个元素,你必须在声明或初始化它们时定义大小。 此外,它们一旦创建,就无法调整大小。 鉴于这些原因,数组在 Go 程序中并不常用,但它们是切片和映射的基础。
要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。
package main
import "fmt"
func main() {
var a [3]int
a[1] = 10
fmt.Println(a[0])
fmt.Println(a[1])
fmt.Println(a[len(a)-1])
}
inta[1] = 10a[0]a[len(a)-1]len
声明数组时,还可使用非默认值来初始化数组。 例如,你可使用以下代码来查看和测试语法:
package main
import "fmt"
func main() {
cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}
fmt.Println("Cities:", cities)
}
数组中的省略号
...
q := [...]int{1, 2, 3}
另一种有趣的数组初始化方法是使用省略号并仅为最新的位置指定值。 例如,使用以下代码:
package main
import "fmt"
func main() {
numbers := [...]int{99: -1}
fmt.Println("First Position:", numbers[0])
fmt.Println("Last Position:", numbers[99])
fmt.Println("Length:", len(numbers))
}
请注意数组的长度是 100,因为你为第 99 个位置指定了一个值。 第一个位置打印出默认值(零)。
First Position: 0
Last Position: -1
Length: 100
多维数组
如果需要处理复杂数据结构,请记住 Go 支持多维数组。 让我们创建一个程序,在其中声明和初始化一个二维数组。 使用以下代码:
package main
import "fmt"
func main() {
var twoD [3][5]int
for i := 0; i < 3; i++ {
for j := 0; j < 5; j++ {
twoD[i][j] = (i + 1) * (j + 1)
}
fmt.Println("Row", i, twoD[i])
}
fmt.Println("\nAll at once:", twoD)
}
切片
我们在上一部分了解了数组,并了解了数组是切片和映射的基础。 你稍后就会明白是为什么。 与数组一样,切片也是 Go 中的一种数据类型,它表示一系列类型相同的元素。 不过,与数组更重要的区别是切片的大小是动态的,不是固定的。
切片是数组或另一个切片之上的数据结构。 我们将源数组或切片称为基础数组。 通过切片,可访问整个基础数组,也可仅访问部分元素。
片只有 3 个组件:
array[0]
package main
import "fmt"
func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter2 := months[3:6]
quarter2Extended := quarter2[:7]
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}
[April May June] 3 9
[April May June July August September October] 7 9
追加项
我们了解了切片的工作原理,还学习了它们与数组的相似性。 现在,让我们来了解它们与数组之间有何不同。 第一个区别是切片的大小不是固定的,而是动态的。 创建切片后,可向其添加更多元素,这样切片就会扩展。 稍后你将了解基础数组发生的情况。
append(slice, element)append
package main
import "fmt"
func main() {
var numbers []int
for i := 0; i < 10; i++ {
numbers = append(numbers, i)
fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
}
}
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
cap()
你注意到容量输出中的模式了吗? 当切片容量不足以容纳更多元素时,Go 的容量将翻倍。 它将新建一个具有新容量的基础数组。 无需执行任何操作即可使容量增加。 Go 会自动扩充容量。 需要谨慎操作。 有时,一个切片具有的容量可能比它需要的多得多,这样你将会浪费内存。
删除项
s[i:p]
例如,以下代码会从切片中删除元素:
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
remove := 2
if remove < len(letters) {
fmt.Println("Before", letters, "Remove ", letters[remove])
letters = append(letters[:remove], letters[remove+1:]...)
fmt.Println("After", letters)
}
}
创建切片的副本
copy(dst, src []Type)
slice2 := make([]string, 3)
copy(slice2, letters[1:4])
为何要创建副本? 更改切片中的元素时,基础数组将随之更改。 引用该基础数组的任何其他切片都会受到影响。 让我们在代码中看看此过程,然后创建一个切片副本来解决此问题。
使用下述代码确认切片指向数组,而你在切片中所做的每个更改都会影响基础数组。
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)
slice1 := letters[0:2]
slice2 := letters[1:4]
slice1[1] = "Z"
fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
}
Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]
slice1lettersslice2letters
若要解决此问题,你需要创建一个切片副本,它会在后台生成新的基础数组。 可以使用以下代码:
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)
slice1 := letters[0:2]
slice2 := make([]string, 3)
copy(slice2, letters[1:4])
slice1[1] = "Z"
fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
}
Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]
slice1slice2
8.映射
大体上来说,Go 中的映射是一个哈希表,是键值对的集合。 映射中所有的键都必须具有相同的类型,它们的值也是如此。 不过,可对键和值使用不同的类型。 例如,键可以是数字,值可以是字符串。 若要访问映射中的特定项,可引用该项的键。
声明和初始化映射
mapmap[T]T
package main
import "fmt"
func main() {
studentsAge := map[string]int{
"john": 32,
"bob": 31,
}
fmt.Println(studentsAge)
}
make()
studentsAge := make(map[string]int)
映射是动态的。 创建项后,可添加、访问或删除这些项。 让我们来了解这些操作。
添加项
要添加项,无需像对切片一样使用内置函数。 映射更加简单。 你只需定义键和值即可。 如果没有键值对,则该项会添加到映射中。
make
package main
import "fmt"
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println(studentsAge)
}
studentsAge := map[string]int{}
nil
package main
import "fmt"
func main() {
var studentsAge map[string]int
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println(studentsAge)
}
在映射中使用下标表示法时,即使映射中没有键,你也总会获得响应。 当你访问不存在的项时,Go 不会执行 panic。 此时,你会获得默认值。 可使用以下代码来确认该行为:
package main
import "fmt"
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println("Christy's age is", studentsAge["christy"])
}
在很多情况下,访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。
若要解决上一代码片段遇到的问题,可执行以下代码:
package main
import "fmt"
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
age, exist := studentsAge["christy"]
if exist {
fmt.Println("Christy's age is", age)
} else {
fmt.Println("Christy's age couldn't be found")
}
}
使用第二个代码片段检查映射中的键在你访问之前是否存在。
删除项
delete()
package main
import "fmt"
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
delete(studentsAge, "john")
fmt.Println(studentsAge)
}
正如上述所言,如果你尝试删除不存在的项,Go 不会执行 panic。
映射中的循环
最后,让我们看看如何在映射中进行循环来以编程方式访问其所有的项。 为此,可使用基于范围的循环,如下例所示:
package main
import (
"fmt"
)
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
for name, age := range studentsAge {
fmt.Printf("%s\t%d\n", name, age)
}
}