0.前言
根据上一篇文章的准备,我们现在可以正式学习go语言基础语法啦!
01.你好,世界!
几乎所有编程语言的第一份demo都是打印HelloWorld!
package main
import "fmt"
func main() {
//打印你好世界
fmt.Println("Hello World!")
}
02.常量与变量
定义让人很自然的联想到js,但仅仅只是定义词相似,go语言是强类型语言,可以自动识别数据类型。
package main
import (
"fmt"
"math"
)
func main() {
//变量的声明方法1
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64 //默认值0
//变量的声明方法2
f := float32(e) //类型转换
g := a + "foo" //字符串拼接
fmt.Println(a, b, c, d, e, f)
fmt.Println(g)
//常量的声明
const s string = "constant"
const h = 5000000
const p = 3e20 / h
fmt.Println(s, h, p, math.Sin(h), math.Sin(p))
}
03.for循环
在go语言中只有这一种循环。同样它的三个条件可以省略,实现死循环,配合break和continue语句让程序按业务逻辑循环运行。
package main
import (
"fmt"
)
func main() {
//所有的判断条件均可省略
for {
fmt.Println("loop")
break //强制结束死循环
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue //跳出本次循环
}
fmt.Println(n)
}
//改造成“while循环”
i := 1
for i <= 3 {
fmt.Println(i)
i += 1 //等价于在23行i++
}
}
04.if判断
判断条件无需括号
package main
import (
"fmt"
)
func main() {
//if-else判断,执行内容必须被大括号包裹
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")
}
}
05.switch分支
判断条件和实现逻辑十分灵活
package main
import (
"fmt"
"time"
)
func main() {
//无需break,遇到符合条件的case自动切断
//通过fallthrough强制执行符合条件的case及后续case
svalue1 := 2
switch svalue1 {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
fallthrough
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}
//case判断可以使用变量,字符串等
t := time.Now()
//在switch后面不加任何变量,直接在case中写条件分支,实现清晰的代码逻辑
switch {
case t.Hour() < 12:
fmt.Println("It is before noon")
default:
fmt.Println("It is after noon")
}
//根据数据类型跳转
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
}
06.数组
一般长度是固定的
package main
import (
"fmt"
)
func main() {
var x [5]int
x[4] = 100
//索引值从0开始,未初始化的数组值是0
fmt.Println(x[4], len(x))
y := [5]int{1, 2, 3, 4, 5}
fmt.Println(y)
var twoDimension [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoDimension[i][j] = i + j
}
}
fmt.Println("二维数组:", twoDimension)
}
07.切片
切片相当于变长数组,但截取逻辑与python不同,即不支持负索引
package main
import "fmt"
func main() {
//使用make创建切片
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
//执行append操作时,若容量不够会自动扩充且返回新的slice
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]
c := make([]string, len(s))
//copy(新,旧)
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]
}
08.map
格式是map([KeyType]valueType)
package main
import "fmt"
func main() {
//字典、哈希
//map([KeyType]valueType)
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
//加入ok,代表是否能找到相符的key值
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
//删除key值
delete(m, "one")
//遍历遵循随机原则
//定义map时,若直接初始化则无需借助make()创建切片
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
09.range
"_"表示占位,虽然有时候可以省略,但为了让结构更加清晰,我们还是不建议省略。
package main
import "fmt"
func main() {
//for后的两个条件,分别对应key和value,可以用_占位或直接省略
nums := []int{2, 3, 4} //定义数组只需value值
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
fmt.Println(sum) // 9
m := 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
}
for k := range m {
fmt.Println("key", k) // key a; key b
}
}
10.函数
函数的返回值可以额外指定一个,用于返回状态,如果是ok表示执行无误。
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
// 当参数类型相同,可同时指定
func add2(a, b int) int {
return a + b
}
// 返回值1为真正的返回结果,返回值2是错误信息
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) // 3
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
}
11.指针
作用有限,一般用于调用函数时将修改形参的值
package main
import "fmt"
func add2(n int) {
n += 2
}
// 使用指针,执行完后会改变全局变量的值
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}
12.结构体
这里突然感觉像创建表结构,将字段名、键值用{}集中定义
package main
import "fmt"
// 结构体是带类型的字段集合
type user struct {
name string
password string
id string
}
func main() {
a := user{name: "wang", password: "1024", id: "001"}
b := user{"wang", "1024", "002"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
fmt.Println(checkPassword(a, "haha")) // false
fmt.Println(checkPassword2(&a, "haha")) // false
//对于没有返回值的方法不能直接放在输出语句中
//错误示例 fmt.Println(resetId(&a,"999"))
resetId(&a, "999")
fmt.Println(a.id)
}
func checkPassword(u user, password string) bool {
return u.password == password
}
// 以上两者方法等效
func checkPassword2(u *user, password string) bool {
return u.password == password
}
// 指针实现对结构体的修改
func resetId(u *user, newId string) {
u.id = newId
}
13.结构体方法
package main
import "fmt"
type user struct {
name string
password 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
}
14.错误处理
错误处理 在
go语言里面符合语言习惯的做法就是使用一个单独的返回值来传递错误信息。不同于Java使用的异常。go语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的if else来处理错误。在函数里面,我们可以在那个函数的返回值类型里面,后面加一个error, 就代表这个函数可能会返回错误。那么在函数实现的时候,return需要同时返回两个值,要么就是如果出现错误的话,那么可以return nil和一个error。如果没有的话,那么返回原本的结果和nil。
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password 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("User not found")
//变量u表示当前迭代的用户信息,而切片users是所有用户的集合。
// 在这个函数中,参数users是一个切片类型的变量,用于存储用户信息。
而返回值v是一个指向用户类型的指针,用于表示找到的用户。
// 如果找到了匹配的用户,则把该用户的地址赋值给v;
如果未找到匹配的用户,则v将为nil。
// 同时,返回值err是一个error类型的变量,用于表示是否发生了错误。
// 如果成功找到用户,则err为nil;如果未找到用户,
则返回一个自定义的错误信息"User not found"。
}
/*
在这段代码中,使用指针的目的是为了避免在函数返回时复制大量的数据。
当函数需要返回一个较大的结构体或对象时,将其作为值返回会导致数据的复制,
从而产生额外的开销。而使用指针作为返回值,则只需要返回该数据的内存地址,避免了数据的复制。
在findUser函数中,当找到与给定姓名匹配的用户时,通过返回&u将该用户的地址作为指针返回。
这样,在main函数中接收返回值时,可以得到指向原始用户结构体的指针,
而不是创建一个新的结构体进行复制。这样可以提高性能并减少内存占用,特别是当用户结构体较大时。
此外,还可以使用指针来可以修改原始数据。在这个示例中,通过返回用户结构体的指针,我们可以在main函数中修改用户的属性。
例如,可以通过u.name = "new name"来更改用户的姓名。
总而言之,使用指针作为返回值可以提高效率、节省内存,并允许对原始数据进行修改。
但需要注意确保指针指向的数据在函数调用后仍然有效,避免出现悬空指针或内存泄漏等问题。
*/
func main() {
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // wang
if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(u.name)
}
}
15.字符串处理
常用的方法都总结在注释里了,和python很像,但是go的包更加灵活,版本管理也简单了许多。
package main
import (
"fmt"
"strings"
)
// strings.Contains(a, "ll"):判断字符串a中是否包含子串"ll",返回true或false。
// strings.Count(a, "l"):计算字符串a中子串"l"的出现次数,返回2。
// strings.HasPrefix(a, "he"):判断字符串a是否以"he"开头,返回true。
// strings.HasSuffix(a, "llo"):判断字符串a是否以"llo"结尾,返回true。
// strings.Index(a, "ll"):返回子串"ll"在字符串a中首次出现的位置,返回2。
// strings.Join([]string{"he", "llo"}, "-"):将字符串切片["he", "llo"]使用"-"连接起来,返回"he-llo"。
// strings.Repeat(a, 2):将字符串a重复2次,返回"hellohello"。
// strings.Replace(a, "e", "E", -1):将字符串a中的"e"替换为"E",返回"hEllo"。
// strings.Split("a-b-c", "-"):将字符串"a-b-c"按"-"分割成字符串切片,返回["a", "b", "c"]。
// strings.ToLower(a):将字符串a转换为小写,返回"hello"。
// strings.ToUpper(a):将字符串a转换为大写,返回"HELLO"。
// len(a):返回字符串a的长度,返回5。
// len(b):返回字符串b的长度,返回6。注意,字符串b中包含的是中文字符,一个中文字符占3个字节。
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
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo,-1表示替换所有,0不替换,正整数表示替换前n个匹配的子串
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
}
16.格式化输出
%v可以自动判断数据类型,
package main
import "fmt"
type point struct {
x, y int
}
// %v:通用占位符,根据值的类型自动选择合适的格式进行输出。
// %s:用于字符串,将字符串按原样输出。
// %d:用于有符号整数类型(int、int8、int16、int32、int64),按十进制输出。
// %f:用于浮点数类型(float32、float64),按默认精度输出。
// %t:用于布尔类型,输出true或false。
// %c:用于字符类型,输出对应的字符。
// %p:用于指针类型,输出指针地址。
// %b:用于整数类型,按二进制输出。
// %o:用于整数类型,按八进制输出。
// %x或%X:用于整数类型,按十六进制输出,%x输出小写字母,%X输出大写字母。
// %e或%E:用于浮点数类型,按科学计数法输出,%e输出小写字母e,%E输出大写字母E。
// %q:用于字符串类型,输出带有双引号的字符串。
func main() {
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
}
17.json处理
在定义结构体时,
json:"age"是一个结构体字段的标签(tag),用于指定该字段在JSON序列化和反序列化过程中的名称或其他相关选项。 在Go语言中,结构体的字段可以通过标签来为字段附加元数据。在这个例子中,Age字段的标签是json:"age",它告诉编码和解码JSON时使用age作为字段的名称。 这在处理JSON数据时非常有用。当我们将结构体实例编码为JSON时,Age字段将使用age作为键名;当我们从JSON解码到结构体时,会根据age键名将对应的值赋给Age字段。 标签不仅仅用于JSON序列化和反序列化,还可以用于其他用途,如数据库映射等。
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age 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 userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
18.时间处理
time.Parse函数用于将字符串解析为time.Time类型的时间值。第一个参数是时间的格式,第二个参数是要解析的字符串。 在这个例子中,时间的格式是"2006-01-02 15:04:05",而要解析的字符串是"2022-03-27 01:25:36"。 如果解析成功,将返回解析后的time.Time类型的时间值t3,否则将返回一个非空的错误err。fmt.Println(t3 == t)用于比较解析后的时间值t3和之前定义的时间值t是否相等。在这个例子中,它会输出true,表示两个时间值相等。fmt.Println(now.Unix())用于输出当前时间的Unix时间戳。Unix时间戳是从1970年1月1日UTC时间开始经过的秒数。在这个例子中, 需要注意的是,时间比较需要确保两个时间值的时区一致,否则比较可能不准确。在这个例子中,t3和t都使用的是本地时区。
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
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
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 := 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)
}
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1648738080
fmt.Println(time.Time{})
}
19.格式转换
strconv.ParseFloat函数用于将字符串转换为float64类型的浮点数。第一个参数是要转换的字符串,第二个参数是指定转换结果的位数。strconv.ParseInt函数用于将字符串转换为int64类型的整数。 第一个参数是要转换的字符串,第二个参数是字符串表示的整数的进制(如10表示十进制,16表示十六进制),第三个参数是指定转换结果的位数。strconv.Atoi函数用于将字符串转换为int类型的整数。它是strconv.ParseInt的一个简便方法,第一个参数是要转换的字符串。 需要注意的是,这些转换函数在转换失败时会返回一个错误。在示例中,我们使用了一个匿名变量_来忽略错误。
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
// 在这个例子中,它将字符串"1.234"转换为浮点数1.234。
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64) //若第二个参数为0,则自动判断进制
fmt.Println(n) // 4096
// 在这个例子中,它将字符串"111"转换为十进制整数111,将字符串"0x1000"转换为整数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
}
// 在这个例子中,它将字符串"123"转换为整数123,将字符串"AAA"转换失败,因为"AAA"不是一个有效的整数字符串,所以它返回零值0和一个错误。
20.进程信息
配合程序调试,性能优化
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// go run example/20-env/main.go a b c d
fmt.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
}
21.实战案例——猜数游戏
经过上述基础代码的学习,我们可以把学习其他语言的经验迁移到go中,发现它的语法更简便,功能更强大;轻量级,多线程的特性与当今的万物互联的世界完美契合!
接下来,我们将完成“猜数小游戏”熟悉语法规则。游戏的架构是系统生成一个0-100的随机数,根据用户的输入来判断输入数与随机数的关系(大于小于则提示,等于则提示并退出程序)
package main
import (
"fmt"
"math/rand"
)
//version1.0
func main() {
maxNum := 100
secretNumber := rand.Intn(maxNum)
fmt.Println("神秘数是 ", secretNumber)
}
使用rand.Intn(maxNum)生成一个范围在0到maxNum-1之间的随机数,并将其赋值给secretNumber。最后,我们使用fmt.Println打印出生成的随机数。
go1.20版本debug截图:
在1.0版本已经能够生成一个0-100的随机数。
但是重复运行程序,结果不变,这是为什么呢?(go1.20后续版本已弃用seed) 通过查阅相关文档,我们需要添加````rand.Seed(time.Now().UnixNano())```
接下来,开始v2版本的迭代。本次支持用户输入并对可能发生的错误进行了捕捉和处理。
package main
//1.19.7
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
secretNumber := rand.Intn(maxNum) // 生成一个0到maxNum-1之间的随机数作为秘密数字
fmt.Println("神秘数是 ", secretNumber) // 打印秘密数字
fmt.Println("请输入你要猜测的数字:") // 提示用户输入猜测的数字
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n') // 从标准输入(键盘)读取一行用户输入
if err != nil {
fmt.Println("一个错误在您输入时发生了,请重试,错误代码为:", err)
return
}
input = strings.Trim(input, "\r\n") // 去除输入字符串中的换行符和回车符
guess, err := strconv.Atoi(input) // 将用户输入的字符串转换为整数类型
if err != nil {
fmt.Println("无效的输入,请输入一个100以内的正整数")
return
}
fmt.Println("你猜的数是", guess) // 打印用户猜测的数字
}
附上运行截图:
注意: 在1.20版本后读取输入的方式由
bufio.NewReader(os.Stdin)改为fmt.Scanf("%d", &guess)。但是对于例如36.3的浮点数,sanf会认为是36和6.为了避免这个bug,且在不改变游戏逻辑的情况下,我将输入修改为Sanln。官方文档-Scan
最后,加入分支判断和循环,完成游戏的调试!
package main
import (
"fmt"
"math/rand"
)
func main() {
maxNum := 100
secretNumber := rand.Intn(maxNum) // 生成一个0到maxNum-1之间的随机数作为秘密数字
// fmt.Println("神秘数是 ", secretNumber)
fmt.Println("请输入你要猜测的数字:") // 提示用户输入猜测的数字
var guess float64 // 使用浮点数类型存储用户输入的数字
for {
_, err := fmt.Scanln(&guess) // 从标准输入(键盘)读取用户输入的浮点数值
if err != nil {
fmt.Println("一个错误在您输入时发生了,请重试,错误代码为:", err)
continue
}
fmt.Println("你猜的数是", guess) // 打印用户猜测的数字
if guess > float64(secretNumber) {
fmt.Println("你猜的数字大于神秘数,请重试")
} else if guess < float64(secretNumber) {
fmt.Println("你猜的数字小于神秘数,请重试")
} else {
fmt.Println("猜对了,恭喜你!")
break
}
}
}
运行截图: