这是我参与「第五届青训营」伴学笔记创作活动的第 1 天
HelloWorld
package main // go app 都有一个 main 包
import (
"fmt" // 引入标准库的 fmt 包 (format, 格式化输出)
)
// go app 入口函数
func main() {
fmt.Println("Hello World)
}
编译:编译成纯二进制文件,直接运行二进制文件即可,不需要其他任何依赖(不像Java还要拖一个JVM)
go build xxx.go # 编译
./xxx # 运行
直接运行:
go run xxx.go # 编译成临时二进制文件后运行, 体量比 go build 小, 速度比 go build 快
变量与数据类型
变量声明与赋值:
// 声明
var a int
// 赋值
a = 10
// 声明同时赋值, var 变量名 变量类型 = 变量值
var a int = 10
// 或者 自动判断数据类型
var a = 10
// 声明并赋值,省略 var
a := 10
数据类型:int
、float
、float
、bool
、string
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64
f := float32(e) // 强制类型转换
g := a + "foo"
常量:
const s string = "constant"
const h = 5000
const l = 12.34
分支语句
与Java不同点:if
后没有小括号()
,必须有花括号{}
,即不能写成一行。
if 7 % 2 == 0 {
fmt.Println("7 % 2 == 0")
} else {
fmt.Println("7 % 2 != 0")
}
Go 的if
语句可以在判断前添加一个执行语句:
if n := 9; n < 10 {
// do some ...
} else if (n > 0) {
// ...
} else {
// ...
}
Go 中的switch
结构,某个case
后不加break
也不会一直往下执行,这是与 C/C++/Java 不同的地方。
package main
import (
"fmt"
"time"
)
func main() {
a := 2
switch a {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
default:
fmt.Println("other")
}
// switch 的功能更加强大
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
循环
Go中只有for
循环,没有while
或do while
循环。
// 死循环
for {
}
// 经典 for 循环
for i := 0; i < 10; i++ {
fmt.Println("i: ", i)
}
// while 循环的效果
i := 0
for i < 3 {
// do some ...
i++
}
for j := 0; j++ {
// 死循环, j 一直加
fmt.Println("j", j)
}
数组
var a [5]int // 声明 5 个元素,类型为 int 的数组
a[4] = 100 // 操作数组元素
// 声明并赋值
b := [5]int{1, 2, 3, 4, 5}
var c [2][3]int // 二维数组
实际开发中很少使用长度固定的数组,更多的使用的是动态的切片。
切片(Slice)
就像 Java 的ArrayList<>
,Python 的list
,底层是数组,但是可以动态改变长度。
s := make([]string, 3) // 使用 make , 基于一个 string 数组创建切片
s[0] = "a"
s[1] = "abc"
s[2] = "e"
s = append(s, "d") // 追加元素, 注意要赋值回来
s = append(s, "ef", "gh") // 追加多个元素
fmt.Println(s[1:4]) // 支持和 Python 一样的切片操作
fmt.Println(s[:4])
fmt.Println(s[2:])
// Tips:不支持负数从末尾开始索引
Map
m := make(map[string]int) // 使用 make 创建 map
m["one"] = 1
m["two"] = 2
fmt.Println(m["one"])
fmt.Println(len(m))
fmt.Println(m["abcd"]) // 没有对应的 key , 得到的是 int 默认值 0
// 有对应 key 得到的是对应的 value 和 true , 否则得到的是类型默认值和 false
val, ok := m["abcd"]
delete(m, "one")
// 声明并赋值
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
使用make
可以预先分配内存空间,在可以预计需要存储的元素的数量的情况下使用make
指定元素数量创建Slice
或Map
可以减少因需要扩容而重新分配内存的次数,从而提高执行速度。
Range
用于快速遍历数组、slice
和map
。
nums := []int{1, 2, 3, 4}
sum := 0
for i, n := range nums {
sum += n
fmt.Println(i) // 0 1 2 3
}
fmt.Println(sum) // 10
// 使用下划线可以忽略前面的某个值
for _, n := range nums {
fmt.Println(n)
}
m := map[string]int{"one": 1, "two": 2}
for k, v := range m {
fmt.Println(k, v)
}
for k := range m {
fmt.Println(k)
}
for _, v := range m {
fmt.Println(v)
}
// PS: 遍历 Map 是完全无序的,顺序是随机的
函数
Go 的函数:
- 类型是后置的
- 可以返回多个值,通常第一个值是真实的返回值,第二个值返回可能出现的错误
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
}
指针
- 基本数据类型,变量存储的就是值,也叫值类型
- 获取变量所在的内存空间的地址,用 & ,例如:
var i int = 10
// 获取 i 的地址:&i
fmt.Println("变量 i 在内存中的地址:", &i)// 0x140000a4008
- 指针类型,变量存的是一个地址,这个地址指向的内存空间存的才是变量的值,例如:
var ptr *int = &i
// 1、ptr 是指针变量
// 2、ptr 的类型是 *int
// 3、ptr 的值是变量 i 在内存中的地址
fmt.Println(ptr)// 0x140000a4008
fmt.Println(&ptr)// 0x1400009e020
- 获取指针类型所指向的值,使用:* ,例如:
// ptr 指向的值
fmt.Printf("ptr指向的值:", *ptr) // 10
关于方法的值传递,进一步理解指针:
func add(n int) {
n += 2
}
func add2(n *int) {
*n += 2
}
func main() {
n := 1
add(n)
fmt.Println(n) // 1
add2(&n)
fmt.Println(n) // 3
}
结构体
带字段的类型的集合。类似面向对象语言中的类,C中的结构体。
package main
import "fmt"
type user struct {
id string
name string
}
func main() {
u1 := user{id: "1", name: "zhangsan"}
u2 := user{"2", "lisi"}
u3 := user{name: "wangwu"} // 未初始化的字段则会指定为默认值
u3.id = "3"
var u4 user // 声明 user 类型的变量 u4
u4.id = "4"
u4.name = "zhaoliu"
fmt.Println(u1, u2, u3, u4) // {1 zhangsan} {2 lisi} {3 wangwu} {4 zhaoliu}
fmt.Println(u1.name) // zhangsan
// ...
}
结构体方法,类似面向对象语言中的类的成员方法。
package main
import "fmt"
type user struct {
id string
name string
}
func (u user) sayHello() {
fmt.Println("My name is ", u.name, ", my id is ", u.id)
}
func main() {
u1 := user{id: "1", name: "zhangsan"}
u1.sayHello()
}
带指针的结构体方法,注意修改结构体内容必须是带指针的结构体方法,与面向对象的类成员方法不同:
package main
import "fmt"
type user struct {
id string
name string
}
func (u user) setName1(name string) {
u.name = name
}
func (u *user) setName2(name string) {
u.name = name
}
func main() {
u1 := user{id: "1", name: "zhangsan"}
u1.setName1("lisi")
fmt.Println(u1) // {1 zhangsan}
u1.setName2("wangwu")
fmt.Println(u1) // {1 wangwu}
}
错误处理
Go 没有try...catch...
,Go的错误处理是由函数主动通过第二个返回值返回是否有错误信息。
package main
import (
"errors"
"fmt"
)
// 返回值类型中加上了error类型,即代表这个函数可能出现错误
// 返回的时候要返回两个值,其中err在函数体内直接使用即可,而不需要主动声明
func hasNum(nums []int, target int) (index int, err error) {
idx := -1
for i, n := range nums {
if n == target {
idx = i
break
}
}
if idx == -1 {
err = errors.New("not found")
}
return idx, err
}
func main() {
nums := []int{1, 2, 3, 4, 5}
idx1, err1 := hasNum(nums, 4) // 3 <nil>
fmt.Println(idx1, err1)
idx2, err2 := hasNum(nums, 6) // -1 not found
fmt.Println(idx2, err2)
if err2 != nil {
// 错误处理...
}
}
简单错误是仅出现一次的错误,且在其他地方不需要捕获该错误。对于简单错误,直接这样通过errors.New
来创建即可,如果有格式化需求,则可以使用fmt.Errorf
。
对于复杂的错误,可以采用错误的Wrap
和UnWrap
。错误的Wrap
实际上是提供了一个error
嵌套另一个error
的能力,从而形成一个error
的跟踪链。具体操作为在fmt.Errorf
中使用%w
将一个错误关联至错误链中。
func Foo() (err error) {
// do some...
res, err2 := OtherFoo()
if err2 != nil {
return fmt.Errorf("has xxx error: %w", err2)
}
}
对于判断错误是否为特定错误,可以使用errors.Is
:
data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
return []byte{}, nil
}
return data, err
在错误链上获取特定种类的错误,使用errors.As
:
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
当程序真正发生不可逆转的错误,导致程序已经无法正常工作时,使用panic
。
res, err := Foo()
if err != nil {
// panic(err)
// 打上日志再 panic(err)
log.Panicf("Error:%v", err)
}
recover
函数:
- 是在
defer
中的内置函数。 - 只能在被
defer
的函数中使用。 defer
是函数最后执行的函数。- 嵌套无法生效。
- 只在当前协程生效。
- 多个
defer
语句的顺序是后进先出。 recover
会使程序从panic
中恢复,导致panic
异常的函数不会继续执行,但能正常返回。defer
定义在panic
函数之前,因为panic
之后的内容都不会执行,包括之后的defer
。recover
的返回值为panic
的错误,如果没有发生panic
,则会返回nil
。
func Foo() (err error) {
defer func() {
// 当 Foo 函数中出现 panic 时,调用 recover 函数就会获取到错误
// debug.Stack() 可以记录调用栈
if e := recover(); e != nil {
err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
}
}
}
字符串操作
在strings
包中有很多字符串操作函数。
package main
import (
"fmt"
"strings"
)
func main() {
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
// 将 a 字符串重复两次得到一个新的字符串
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)) // 6, unicode一个中文由三个字符编码
}
字符串格式化
在 Java 中,System.out.printf(...)
中可以用%d
表示数字,字符串用%s
等。而 Go 中可以用%v
表示任意类型。
package main
import "fmt"
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{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}
// %+v 可以显示更详细的信息
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
// %#v 可以显示比 +v 更详细的信息
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
}
JSON处理
只要保证结构体中每个字段的第一个字母是大写(Golang中的公开字段的表示方式(public)),就能通过encoding/json
包下的Marshal
方法序列化成Json格式的字符串的byte
数组。
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"` // 指定序列化成Json时的key用age
Hobby []string
}
func main() {
a := userInfo{"zhangsan", 18, []string{"Golang", "Java"}}
buf, err := json.Marshal(a)
if err != nil {
fmt.Println(err)
}
fmt.Println(buf) // [123 34 78 97 109 101 34 58 34 122 104 ...]
fmt.Println(string(buf)) // {"Name":"zhangsan","age":18,"Hobby":["Golang","Java"]}
// 得到格式化(缩进、换行)的Json
buf2, err := json.MarshalIndent(a, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf2))
//{
// "Name": "zhangsan",
// "age": 18,
// "Hobby": [
// "Golang",
// "Java"
// ]
//}
// 反序列化成结构体对象
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"zhangsan", Age:18, Hobby:[]string{"Golang", "Java"}}
}
时间处理
time
包下的函数用于时间处理。
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now() // 获取当前时间
fmt.Println(now)
// 构造指定时间
t := time.Date(2023, 1, 1, 12, 25, 36, 0, time.UTC)
fmt.Println(t)
// 获取时间信息
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute())
// 格式化时间为 yyyy-MM-dd HH:mm:ss 格式, Format函数的参数是固定的
fmt.Println(now.Format("2006-01-02 15:04:05"))
// 时间减法
diff := now.Sub(t)
fmt.Println(diff, diff.Minutes(), diff.Seconds())
// 指定时间格式和时间字符串, 将时间字符串转换为时间对象
t2, err := time.Parse("2006-01-02 15:04:05", "2023-01-15 12:38:36")
if err != nil {
panic(err)
}
fmt.Printf("%#v", t2)
// 获取时间戳
fmt.Println("\n", now.Unix())
}
数字解析
strconv
包下的函数用于字符串的数字解析。
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ = strconv.ParseFloat("1.234", 64) // 转为 float64
n, _ = strconv.ParseInt("123", 10, 64) // 转为 10 进制, int64
n2, _ = strconv.Atoi("356") // 自动转换
}
进程信息
os
包下的函数用于获取进程信息。
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// 命令行参数
// go run xxxxxx.go args1 args2 ...
fmt.Println(os.Args)
// 环境变量
fmt.Println(os.Getenv("PATH"))
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))
}