这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
Golang 基础语法
Go 语言的特点
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库
- 完善的工具链
- 静态链接
- 快速编译
- 跨平台
- 垃圾回收
Golang 的安装
Golang 的安装与使用 | Wen Flower 学习笔记 (twtool.icu))
基础语法
编写简单的 Hello World 代码
package main
import "fmt"
func main() {
// Go 语言内置了 println 函数, 但是使用的标准错误输出, 同时会有输出顺序和预期不一致的问题
// println("Hello World!")
// 通过 fmt 标准库可以将内容输出到标准输出流
fmt.Println("Hello World!")
}
变量
Go 语言变量声明的语法为: var <name> <type>
也可以在声明变量的同时对变量赋值语法为: var <name> [type] = <value> 或者 <name> := <value>
- <> : 必须
- [] : 可选, 因为第二种方式对变量进行了赋值, Go 语言能推断出变量的类型
- name : 为变量名
- type : 变量类型
- value : 要赋值给变量的值
代码:
package main
import "fmt"
func main() {
var a int
a = 1
var b int = 2
var c = 3
d := 4
var e, f int
e, f = 5, 6
g, h := 7, 8
fmt.Println("a = ", a, ", b = ", b, ", c = ", c, ", d = ", d, ", e = ", e, ", f = ", f, ", g = ", g, ", h = ", h)
}
常量
Go 语言常量声明和变量声明语法类似, 但是必须在声明初始化:
const <name>[, <name>]... [type] = <value>[, <value>]...
- <> : 必须
- [] : 可选, 可选, 对常量进行了赋值, Go 语言能推断出常量的类型
- name : 为常量名
- type : 常量类型
- value : 要赋值给常量的值, 且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型
代码:
package main
import "fmt"
func main() {
const a = 1
const b, c = 2, 3
fmt.Println("a = ", a, ", b = ", b, ", c = ", c)
// 通过 iota 语法声明自增的常量
const (
d = 4 + iota // 4
e // 5
)
fmt.Println("d = ", d, ", e = ", e)
// 通过 iota 声明常量
const (
f = 1 << iota // 1
g = 1 << iota // 2
h = 1 << iota // 4
)
fmt.Println("f = ", f, ", g = ", g, ", h = ", h)
}
分支语句
Go 语言的 if-else 语法为:
if [变量声明与赋值表达式;] <布尔表达式> {
} else if [变量声明与赋值表达式;]<布尔表达式>{
} else {
}
else if 和 else 是可选的, Go 的 if 不需要小括号(), 但是不能省略花括号 {}
Go 语言的 switch 语法为:
switch [value] { // 一个值, 允许不传
case <condition>: // condition 既可以是一个值, 也可以是一个条件表达式
// 操作
default:
// 没有 case 匹配到的操做
}
特别的, Go 语言在 case 后默认不会继续执行其它 case, 需要在 case 中加上 fallthrough 关键字
代码:
package main
import "fmt"
func main() {
if score := 80; score >= 90 {
fmt.Println("优秀")
} else if score >= 60 {
fmt.Println("及格") // 将输出及格
} else {
fmt.Println("不及格")
}
// score 在这里不可以访问到
i := 2
switch i {
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("i 不是1也不是2")
}
}
循环结构
Go 语言只有 for 一种循环关键字, 但是能用 for 做到其它语言的 fori、while 等循环方式
代码:
package main
import "fmt"
func main() {
// 其他语言的 while 循环
i := 0
for i < 5 {
fmt.Println("i = ", i)
}
// 经典 for 循环
for j := 0; j < 5; j++ {
fmt.Println("j = ", j)
}
// loop 循环
for {
fmt.Println("Loop, 不会退出循环, 需要使用 break, 才能退出")
}
}
数组 & 切片
数组
Golang 的数组和其它语言类似,都是定长不能扩容,访问不能超出索引的范围
数组的创建
数组和变量的定义方式类似:var <name> <[<length>]type>
也可以在声明数组的同时对数组赋值,语法为: <name> := [<length>]<type>{[<value>[, <value>]...]}
- <> : 必须
- [] : 可选
- name : 为数组名
- type : 数组类型
- value : 要赋值给数组对应位置的值
- length : 数组的长度,第二种方式可以是(
...)根据后面的值确定长度
package main
import "fmt"
func main() {
var a1 [5]int
a1[2] = 2
fmt.Printf("%v", a1) // [0 0 2 0 0]
// 在编译阶段就会被发现
// a1[5] = 5 // invalid argument: index 6 out of bounds [0:5]
// 下面的代码会在运行时产生 panic
// var i = 1
// for {
// i++
// a1[i] = i // panic runtime error: index out of range [5] with length 5
// if i == 5 {
// break
// }
// }
fmt.Printf("数组 a1 的长度是:%v", len(a1)) // 通过 len 函数能获取数组的长度
fmt.Printf("数组 a1 的容量是:%v", cap(a1)) // 通过 cap 函数能获取数组的容量,因为数组是定长的,所以容量和长度是相同的
// 使用第二种方式创建数组的几种特殊写法
a2 := [5]int{1, 2} // [1, 2, 0, 0, 0]
a3 := [..]int{1, 2, 3} // [1, 2, 3], len(a3) == 3
a4 := [5]int{1:1, 3:3} // [0, 1, 0, 3, 4]
a5 := [...]int{1:1, 5:5} // [0, 1, 0, 0, 0, 5], len(a5) == 6
}
多维数组
Golang 和其它语言一样个支持数组的类型还是一个数组,达到多维数组的效果
package main
import "fmt"
func main() {
// 这里以二维做示例,最前面的中括号为最外层
arr := [2][3]int{
[3]int{1, 2, 3},
[3]int{4, 5, 6}, // 逗号必须有
}
// 使用 for range 遍历,其中 value 可以省略
for _, a := range arr {
for _, value := range a {
fmt.Printf("%v, ", value)
}
fmt.Println()
}
}
切片
Golang 切片类似 Java 语言的 List,会在容量不够时,自动进行扩容,底层实现为数组,切片为引用类型,直接获取切片的地址为数组的地址
切片的创建
切片能够直接声明,使用也能够通过,make 函数创建有初始大小和容量的切片,通过 new 函数创建的切片不能直接使用
代码:
package main
import "fmt"
func main() {
// 切片和数组类似,但是不需要中括号的长度,
// 声明一个空切片,可以直接使用
var slice []int
slice = append(slice, 1)
fmt.Println(slice[0]) // 1
// 切片和数组一样访问不能超出索引的范围,否则会产生:
// panic: runtime error: index out of range [1] with length 1
// fmt.Println(slice[1])
// 使用 make 创建切片
// 第一个参数(必须):切片的类型
// 第二个参数(必须):切片的初始大小,填充默认值
// 第三个参数(可选):切片的容量
makeSlice := make([]int, 5, 10)
fmt.Printf("%v \n", makeSlice) // [0 0 0 0 0]
}
追加元素
Golang 提供了 append 函数在切片的末尾添加元素,就和创建中使用的方式一样
代码:
package main
import "fmt"
func main() {
slice := make([]int, 0, 5)
slice = append(slice, 1)
fmt.Println(slice[0]) // 1
fmt.Printf("%p, cap = %v\n", slice, cap(slice)) // 输出切片引用的数组的地址
slice = append(slice, make([]int, 4)...)
fmt.Printf("%p, cap = %v\n", slice, cap(slice)) // 上一步追加操作,切片的容量还足够,地址不会改变
slice = append(slice, make([]int, 4)...)
fmt.Printf("%p, cap = %v\n", slice, cap(slice)) // 上一步操作,切片的容量不够了,则会改变切片引用的数组的地址,并将容量扩容到原来的两倍
fmt.Printf("%v \n", slice) // [1, 0, 0, 0, 0, 0, 0, 0, 0]
}
删除元素
Golang 提供了没有提供删除函数,但是可以通过 [a:b] 无法做到删除效果,配合 append 方法能不改变切片应用的内存地址
代码:
package main
import "fmt"
func main() {
slice := []int{1,2,3,4}
fmt.Printf("p = %p, len = %v, cap = %v\n", slice, len(slice), cap(slice))
slice = slice[1:3] // 删除头尾元素
fmt.Printf("p = %p, len = %v, cap = %v\n", slice, len(slice), cap(slice))
// 使用append不会改变当前切片数组的内存地址
slice = append(slice[:0], slice[1:]...) // 删除第一个个元素
fmt.Printf("p =%p, len = %v, cap = %v\n", slice, len(slice), cap(slice))
}
复制切片
Golang 提供了 copy(target, source) 函数对切片进行深拷贝
代码:
package main
import "fmt"
func main() {
slice1 := []int{1, 2, 3, 4} // [1, 2, 3, 4]
slice2 := slice1[:] // [1, 2, 3, 4] 引用的数组地址和 slice1 相同
slice3 := make([]int, 4) // [0, 0, 0, 0]
copy(slice3, slice1) // slice3 为 [1, 2, 3, 4] 引用的数组地址和 slice1 不同
fmt.Printf("slice1 = %v, p = %p\n", slice1, slice1)
fmt.Printf("slice2 = %v, p = %p\n", slice2, slice2)
fmt.Printf("slice3 = %v, p = %p\n", slice3, slice3)
}
map
创建与使用
Golang 提供了通过 make 函数创建空 map 的方法,同时也能使用 map[keyType][valueType]{} 语法创建 map
代码:
package main
import "fmt"
func main() {
// 创建空 map,中括号中为 key 类型,后面的为 value 类型
m := make(map[string]int)
// 为 key 赋值
m["wen"] = 21
m["dian"] = 20
fmt.Println(m) // map[dian:20 wen:21] 无序
fmt.Println(len(m)) // 2
fmt.Println(m["wen"]) // 21
fmt.Println(m["tang"]) // 0
value1, ok1 := m["dian"]
fmt.Println(value1, ok1) // 20 true
value2, ok2 := m["tang"]
fmt.Println(value2, ok2) // 0 false
delete(m, "dian") // 删除 key
value3, ok3 := m["dian"]
fmt.Println(value3, ok3) // 0 false
m2 := map[string]int{"wen": 21, "dian": 20}
fmt.Println(m2)
}
遍历 map 元素
package main
import "fmt"
func main() {
m := map[string]int{"wen": 21, "dian": 20}
// 使用 for range 遍历
for key, value := range m {
fmt.Printf("key = %v, value = %v\n", key, value)
}
// 使用 for range 遍历
for key := range m {
fmt.Printf("key = %v, value = %v\n", key, m[key])
}
}
函数
Golang 的函数类似 Kotlin 的语法,参数的类型写在后面,函数的返回值写在函数小括号() 和 花括号{} 中间,同时 Golang 还支持多函数返回值
特别的 Golang 的 error 也是值,由 Golang 创始人提出:
Errors are values,可以直接通过if-else处理,也可以直接忽略
定义 & 使用 代码:
package main
import (
"errors"
"fmt"
)
func _div(a int, b int) int {
return a / b
}
func div(a int, b int) (v int, err error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return _div(a, b), nil
}
func main() {
value, _ := div(3, 2) // 这里忽略错误
fmt.Printf("%d / %d = %d", 3, 2, value) // 3 / 2 = 1
}
值传递 & 引用传递
Go 语言数据类型的分类
按数据类型分类
- 基本类型:
数值类型、string、bool - 复合类型:
struct、array、slice、map、pointer、chan、function
按数据特点分类
- 值类型:
数值类型、string、bool、struct、array - 引用类型:
slice、map、pointer、chan、function
数值类型:
byte、runc、int、uint、int8、int16、int32、int64、uint8、uint16、uint32、uint64、float32、float64
值传递
值类型传递的是数值本身,不是内存地址,将数据拷贝一份传给变量或者函数参数,对副本的修改不影响源数据
引用传递
引用类型传递的是内存地址,同一个地址可以被多个变量引用,对一个变量进行修改则其它应用同一个地址的变量的值也会一起改变
指针
指向一个已知的地址,因为指针是引用类型,所以可以为 nil
指针的已主要参数的对参数的参数进行修改同时让源值也修改
代码:
package main
import "fmt"
func inc(i *int) {
*i++ // 这里需要在前面加上 *
}
func main() {
i := 0
inc(&i)
fmt.Println(i) // 1
inc(&i)
fmt.Println(i) // 2
}
结构体
定义结构体
type <name> struct {
[<field> <type>]...
}
// 例子
type user struct {
name string
age int
}
使用结构体
Golang 不管是结构体指针还是结构体值,都是直接通过 . 访问结构体的字段,而不需要使用 *
代码:
package main
import "fmt"
type user struct {
name string
age int
}
func main() {
u := user{name: "wen", age: 21}
fmt.Printf("name = %v, age = %v\n", u.name, u.age) // name = wen, age = 21
fmt.Printf("u = %v\n", u) // u = {wen 21}
fmt.Printf("u = %+v\n", u) // u = {name:wen age:21}
fmt.Printf("u = %#v\n", u) // u = main.user{name:"wen" age:21}
}
结构体方法
虽然 Golang 是面向过程的语言,但是提供了在方法名前面通过 (param type) 语法实现结构体方法,type 既可以是结构体也可以是结构体指针,可以理解为将函数的第一个参数前置
代码:
package main
import "fmt"
type user struct {
name string
age int
}
func (u user) String() string {
return fmt.Sprintf("name = %v, age = %v", u.name, u.age)
}
func (u *user) ageInc() {
u.age++
}
func main() {
u := user{name: "wen", age: 21}
fmt.Println(u.String()) // name = wen, age = 21
u.ageInc()
fmt.Println(u.String()) // name = wen, age = 22
}
字符串
Golang 提供了 strings 标准库来对字符串进行各种操作
判断子串是否存在
str := "Hello World!"
fmt.Println(strings.Contains(str, "Hello")) // true
fmt.Println(strings.Contains(str, "Golang")) // false
判断子串出现次数
str := "Hello Golang,啊啊啊!!!"
fmt.Println(strings.Count(str, "l")) // 3
fmt.Println(strings.Count(str, "ll")) // 1
fmt.Println(strings.Count(str, "啊")) // 3
判断前后缀是否存在
str := "Hello Golang!!!"
fmt.Println(strings.HasPrefix(str, "Hello")) // true
fmt.Println(strings.HasSuffix(str, "Golang")) // false
查询子串第一次出现的索引
str := "Hello Golang!!!"
fmt.Println(strings.Index(str, "Hello")) // 0
fmt.Println(strings.Index(str, "Golang")) // 6
fmt.Println(strings.Index(str, "World")) // -1
str = "Hello 世界!!!"
fmt.Println(strings.Index(str, "世")) // 6
fmt.Println(strings.Index(str, "界")) // 9 中文在 UTF-8中占 3个字节
fmt.Println(strings.Index(str, "世界")) // 6
获取字符串长度
str = "Hello 世界!!!"
fmt.Println(len(str)) // 21 中文在 UTF-8 中占 3个字节
fmt.Println(len([]rune(str))) // 11
Json 处理
Golang 提供了 encoding/json 标准库序列和反序列 Json 字符串
代码:
package main
import (
"encoding/json"
"fmt"
)
type user struct {
Name string `json:"name"` // 通过这个方式可以将大写的 Name 映射为小写的 name
Age int `json:"age"`
}
func main() {
u := user{Name: "wen", Age: 21}
buf, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // {"name": "wen", "age": 21}
var u2 user
err = json.Unmarshal(buf, &u2)
if err != nil {
panic(err)
}
fmt.Println(u2) // {wen 21}
}
时间处理
Golang 提供了 time 标准库对时间进行处理
代码:
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now() // 获取当前时间
// 自定义输出格式,默认输出会比较长,还会加上时区
// Golang 将在 1.20 为这个固定的 2006-01-02 15:04:05 添加常量 tiem.DateTime
fmt.Println(t.Format("2006-01-02 15:04:05.999"))
// 获取对应的年月日时分秒
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
// 解析时间
pt, _ := time.Parse("2006-01-02 15:04:05", "2023-01-15 22:05:35")
fmt.Println(pt.Format("2006-01-02 15:04:05"))
// 比较时间
fmt.Println(t.After(pt)) // true
fmt.Println(t.Before(pt)) // false
// 获取时间戳
fmt.Println(t.Unix())
}