这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。主要是对于第一节课课程内容的简单记录,由于我本人是第一次接触go语言,编程水平也比较菜,肯定有错误的地方,欢迎大佬指正。
Go语言优势
- 高性能高并发
- 语法简单、学习曲线平缓 Go语法与c++类似,但省去了c++中一些复杂的部分,如很多括号和分号
- 丰富的标准库 标准库相对于第三方库的优点在于标准库更加稳定,且可以享受到语言更新带来的优势
- 完善的工具链
- 静态链接
- 快速编译 远超c++的编译速度
- 跨平台 不仅支持Windows Mac Linux,还可以用于ios Android开发,在路由器树莓派上也可以运行。
- 垃圾回收 与java类似,Go语言也有垃圾回收机制
Go语言基础语法
变量声明
主要有两种方法:
① 与JavaScript类似,Go语言也可以用var声明变量,Go语言会根据语境判断变量的类型:
var a = "initial" //a:initial
var b, c int = 1, 2 //b:1 c:2
var d = true //d:true
② Go语言还提供了另一种声明变量的方法:
变量名:=值
var e float64 //e:0
f := float32(e) //f:0
g := a + "foo" //initialfoo
常量声明
对于常量的声明,go语言使用的是const
const s string = "constant"
const h = 500000000
const i = 3e20 / h
Go语言比较特殊的是,它的变量类型一般是在变量的后面比如上面声明 const s string = "constant" 在其他语言中我们声明一个字符串类型都是string s = "constant"。这点要注意。
if else
if else对比c语言的if else主要有两点不同。
第一点go语言的if else没有括号,即使是加了括号ide在编译过程中也会帮你删除。
第二点go语言必须{}另起一行,不可以在if else同行后面直接写代码:
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
同时go支持在if中给变量复制:
if num := 9; num < 0 { // 此处给num赋值为9
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
for循环
go语言中的for循环同样不含括号,且for循环的三个部分都可以被省略:
for {
fmt.Println("loop")
break
}
上述代码会正常运行for循环中的代码,遇到break自动退出。
for j := 7; j < 9; j++ {
fmt.Println(j)
}
上述代码就是包含三要素的for循环。输出值为7、8
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
continue 可以跳过本次循环剩下的代码,也就是说上面这段代码在n能够被2整除时不会打印n,n为奇数是会打印n,因此输出值是1、3
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
最后这段代码就是省略的版本,i的值可以在for循环之前赋值,i++的操作可以在循环体内执行。输出值为1、2、3
switch
Go语言的switch功能比较强大,同时与c语言相比也有一些区别:
第一个区别是在c语言中如果我们在一个case分支下不写break的话,那么它会依次执行下面每个分支的代码,而在go语言中即使不写break,在执行完相应case后也会自动退出是switch。
第二个区别是Go 语言的switch可以完成类似if的操作
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
比如这段代码,我们调用time.Now()函数获取当前时间,如果小于12打印:
fmt.Println("It's before noon")
大于12打印:
fmt.Println("It's after noon")
这样的结构相比if else要清晰的多。
数组
数组的声明与java比较类似 数组名 := 数组类型{数组成员}
b := [5]int{1, 2, 3, 4, 5}
二维数组的初始化:
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
上述代码声明了一个两行三列的二维数组,两层嵌套循环赋值,赋值为i+j。输出值为[[0 1 2] [1 2 3]]
切片
Go语言中切片的声明需要使用make函数
s := make([]string, 3)
这里声明了一个string类型的,长度为3的切片。
切片的赋值可以直接用=赋值
s[0] = "a"
s[1] = "b"
s[2] = "c"
用len函数可以获取切片的长度。
当想要延长切片时,必须使用append函数,这是因为切片的实际存储是存储了切片的长度和容量,当容量不足时会发生扩容,所有需要把原切片传给它。
s = append(s, "d")
s = append(s, "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]
map
就是哈希表,主要包含两个元素,即key和value,在map声明时,需要分别说明key和value的数据类型:
m := make(map[string]int)
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
map的赋值也很简单:
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
删除元素:
delete(m, "one")
range
range 函数会遍历 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
上面这段代码中的num := range num就代表遍历num,然后把num的值赋值给num,num的索引赋值给i。
m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
fmt.Println(k, v) // b 8; a A
}
再来看一下map的情况,对map使用range函数返回的是key和value,上面这段代码的意思就是把map的key赋值给了k,map的value赋值给了value,最终打印k和v结果是 b 8; a A
func 函数
要注意变量的类型要在变量名后面
func add(a int, 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
}
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
看上面这段代码,输入值是键值都为string类型的map,以及一个字符串k,输出值是字符串类型的v和布尔值ok。 可以看到这个函数的主要目的是在map中搜索输入值k,如果能搜索到k就会返回k对应的value以及true,如果不能搜索到就会返回false。
指针
Go语言的指针比较简单,以下面两个函数为例
func add2(n int) {
n += 2
}
func add2ptr(n *int) {
*n += 2
}
可以看出这个函数的目的是输出n,然后对n加2。但第一个函数无法完成这个操作,这是因为输入的n是一个复制值,如果想要完成对n加2的操作需要输入它的指针。
结构体
结构体的声明:
type user struct {
name string
password string
}
结构体的初始化:
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
以上方式都是可以对结构体进行初始化的,可以看到在部分初始化方式中没有对结构体中的所有元素进行初始化,这也是合法的,因为对于字符串go语言会自动初始化为空字符串,对与整形或浮点数会初始化为0。
结构体方法
其实就是java中的成员函数,对于我们刚刚定义的user结构体,定义下面两个方法:
func (u user) checkPassword(password string) bool {
return u.password == password
}
func (u *user) resetPassword(password string) {
u.password = password
}
可以看到第一个方法要求输入一个password,返回值是布尔值,来检测密码是否正确。第二个函数是对密码的修改,因此需要用到指针。
错误处理
Go语言的错误处理:
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")
}
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
在声明函数时,要加入一个err的返回值。当err == nil的时候,就正常输出,当err != nil时,打印错误。语法很简单,主要这个nil是什么,我搜寻了一下相关资料nil的意思是无,或者是零值。零值,zero value,是不是有点熟悉?在Go语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。这是每种类型对应的零值:
bool -> false
numbers -> 0
string -> ""
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil
字符串操作
下面是Go语言的字符串操作:
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b))
Contains函数用于判断在字符串a中是否包含字符串b,返回值为bool。
Count函数用于计数字符串a中字符串b出现的次数
HasPrefix函数用于测试字符串a是否以指定字符串b开头
HasSuffix函数用于测试字符串a是否以指定字符串b结尾
Index函数用于在字符串a中搜索b,如果能搜索到b将返回字符串b第一次出现的索引位置,如果搜索不到返回-1
Join函数用于连接两个字符串,第一个参数是需要连接的两个字符串,第二个参数是连接符。
Repeat函数可以复制字符串a b次。
Replace函数可以将a中的指定字符串b替换为c,需要注意的是最后一个参数,最后一个参数是替换次数,如果为-1意为全部替换。同时指定字符串b如果为空,那么将在每个字符前加一个字符串c,比如:
fmt.Println(strings.Replace(a, "", "E", -1)) // EhEeElElEoE
Split函数,经典的字符串分割,第一个参数为需要分割的字符串,第二个参数是分割符。
ToLower函数,全部输出小写字母,与之对应的ToUpper函数,全部输出大写字母。
len函数,输出字符串长度,函数本身没什么好说的,但要注意对于包含汉字的字符串来说,一个汉字可能对应多个字符,所以计算长度可能与预期结果不同。
字符串格式化
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n) // hello 123
fmt.Println(p) // {1 2}
fmt.Printf("s=%v\n", s) // s=hello
fmt.Printf("n=%v\n", n) // n=123
fmt.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.141592653
fmt.Println(f) // 3.141592653
fmt.Printf("%.2f\n", f) // 3.14 保留两位小数的小数
go语言的打印,有println和printf两种,println没什么好说的,打印自动换行。主要说一下printf打印的几个区别。%v打印本身,%+v对于map就会打印键:值,对于结构体就会打印成员:值。%#v则会连对应的数据类型一起打印。
Json处理
json.Marshal()可以直接将数据结构转化为json字符串
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
json.Marshal(a)
fmt.Println(string(buf))
一定要加string啊,不加string打印出来的全是数字这里打印的结果是:
{"Name":"wang","Age":18,"Hobby":["Golang","TypeScript"]}
{
"Name": "wang",
"Age": 18,
"Hobby": [
"Golang",
"TypeScript"
]
}
这里的json也可以自定义,只需要在定义结构体时做如下操作:
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
这时最终转化的json信息Age一栏就将变成age。
时间处理
time.Now函数可以获取当前的时间time
now := time.Now() //获取当前时间
fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
time.Date支持输入一串数字生成时间,同时可以指定时区
t := 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 UTC
t是一个结构体,可以分别打印年、月、日、分钟、小时。
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36
可以使用减法获取一个时间段,通过打印diff.Minutes(), diff.Seconds()可以获取这个两个时间点隔了多少分钟、多少秒。
diff := t2.Sub(t) //通过减法获得一个时间段
fmt.Println(diff) // 1h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
if err != nil {
panic(err)
}
可以直接用==判断两个时间是否相等,可以用now.Unix打印时间戳
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1648738080 时间戳
数字解析
strconv是Go语言提供的用于类型转换的库:
f, _ := strconv.ParseFloat("1.234", 64) //解析字符串
fmt.Println(f) // 1.234
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
ParseFloat和ParseInt可以分别将字符串转化为对应的数据类型。ParseInt的三个参数分别为字符串,进制,需要返回多少精度的整形参数,如果填0Go语言会自动判断数字的进制。 Atoi则只能将字符串转化为固定类型的数字。
进程信息
os.args会获取程序运行时的参数,主要是程序的路径
fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
以我的运行结果为例:
D:\LocalGit\go-by-example\example\20-env\main.go #gosetup
Getenv获取系统环境变量,输入是 key,返回值是相应的 value,如果没有对应的 key,则返回空。
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
Setenv设置环境变量,一般是 key 和 value 成对出现:
fmt.Println(os.Setenv("AA", "BB"))
猜字游戏
猜字游戏比较简单,主要有一些bug,如果有朋友直接git拉下来代码应该是跑不了的
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano()) //使用当前时间戳作为seed生成随机数,不然每次生成的值都是81
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber) 这里是防止直接打印出结果
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
这是最终版本的代码,我debug了一下,主要问题出在27行
input = strings.TrimSuffix(input, "\n")
这里这行代码主要是为了去掉在控制台输入完成后我们输入回车的/n但实际上输入完数字后后面除了/n还有/r,所以这里正确的代码应该是:
input = strings.TrimSuffix(input, "\r\n")
之后就可以正常运行了,我使用的IDE是Goland,不清楚这个问题是否是IDE或者系统的问题。