初识go语言-go的基础语法(二)及猜数字游戏简单实战案例 | 青训营笔记

59 阅读9分钟

封面链接

这是我参与「第五届青训营 」笔记创作活动的第2天

笔者才学疏浅,很多不足之处请多多指出。

前言

在上一篇文章# 初识go语言-go的基础语法(一)| 青训营笔记中我们简单讨论了go的优势,学习了部分常用基础语法。接下来我们将会继续讨论学习剩下的基础语法和做一个简单的实战案例。

基础语法

range

在go里面,range可以对数组和map进行快速遍历:

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
   }
}
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
}

细心观察的话可以发现有点像java里面的foreach(如果有java基础的话)。不过range不能单独出现,后面必须跟一个范围表达式(数组、指向数组的指针、map等),左侧一般跟一个for且需要两个变量接收range的返回值。遍历map和数组range得到的返回值不一样,如果是遍历数组则返回数组下标(上面示例里面的i来接收)和下标对应的值(上面示例里面的num来接收);如果遍历map,range会返回一个k-v键值对(上面示例里面的k和v)。

函数、结构体、指针

与c语言一样,go有函数、结构体和指针。

函数

与c语言的函数不一样的是,go的函数定义要用到func关键字,且返回类型是后置的,简单的函数定义格式为func+函数名(形参)+返回类型+函数体:

func add(a int, b int) int {
   return a + b
}

go里面的函数是支持返回多个值的,我们可以借此来返回一些其他信息:

func exists(m map[string]string, k string) (v string, ok bool) {
   v, ok = m[k]
   return v, ok
}

结构体

go的结构体定义与c语言差不多:

type user struct {
   name     string
   password string
}

go结构体赋值的一种方式,在定义结构体变量的时候直接赋初值:

a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}

go结构体赋值的另一种方式,先定义一个结构体变量,用结构体变量名.要赋值的结构体内的变量来进行赋值:

var d user
d.name = "wang"
d.password = "1024"

go不止支持函数,也支持方法,其中有一种叫结构体方法,一个结构体方法如下:

func (u user) checkPassword(password string) bool {
   return u.password == password
}

func main() {
   a := user{name: "wang", password: "1024"}
   a.resetPassword("2048")
   fmt.Println(a.checkPassword("2048")) // true
}

一个go函数在func关键字的后面,函数名的前面加上一个结构体变量参数就变成了该结构体的一个方法,在方法内部可以调用该结构体变量参数,该结构体的变量可以调用该结构体的方法。

指针

同c语言的指针相比,go的指针没有那么复杂,只对形参进行一些操作和简单的增删改查,首先看一下面的样例:

func add2(n int) {
   n += 2
}
func main() {
   n := 5
   add2(n)
   fmt.Println(n) // 5
   add2ptr(&n)
   fmt.Println(n) // 7
}

可以看到最后的输出结果是5,也就是形参的赋值操作没有生效,这时候就要用到指针了:

func add2ptr(n *int) {
   *n += 2
}

func main() {
   n := 5
   add2ptr(&n)
   fmt.Println(n) // 7
}

修改之后的结果就达到预期了,go的形参不同其他语言一样可以直接进行操作,需要用指针的方式进行增删改查。

error、string、fmt

error

不同于java里面的错误用异常(exception)表示,go里面用error来表示错误:

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")
}

error是可以作为返回值的,_是用来占位的表示不接收该位置的返回值,nil就是其他语言里面的null空的意思。上面的函数就是一个查找,如果找到了就返回值并且error为空,如果没有找到就返回一个空值和一个error的报错信息。

这下面是实际的简单使用:

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)
   }
}

实际使用中,要对函数值进行接收并判断error是否为空然后进行对应的操作,可以用这样的方式实现简单的错误处理。

string

go有着丰富的函数库,导入strings包即可对string类型的数据进行很多快捷操作:

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
//得到该字符串重复n次之后的字符串
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

fmt

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

Println的打印带有自动换行,作用与java的println一致,这里我们看到Println的开头是大写可能会以为这是go的惯用写法。其实我们引入了库fmt,我们调用Println属于外部访问,函数开头大写代表着可以被包外的其他地方访问,否者只能在本文件内可访问,如同java的private和public的区别。Printf的用法和c语言的printf一样支持一些自己需要的打印格式,%v可表示任意类型,%+v和%#v都是打印出变量的详细信息。

json

在其他语言里面我们想使用json格式可能得导入第三方库,如java常用的fastjson,而go的标准库里面内置了json。

将一个变量转为json字符串:

a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)//返回a的json编码
if err != nil {
   panic(err)
}
fmt.Println(buf)         // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

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"}}

将json字符串转回go里面的变量:

var b userInfo
err = json.Unmarshal(buf, &b)//将json编码数据转储为b指向的值中,若b为0或者不是指针会返回一个错误,所以用&符号
if err != nil {
   panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}

time

先看一下样例:

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

相信上面的样例并不难懂,不过需要注意一点,在go里面的时间格式化并非yyyy-MM-dd mm:ss这样的格式,而是固定使用2006-01-02 15:04:05这个时间作为时间格式化的格式。

strconv

strconv类似java里面的装箱工具类,可以对string和int做一些转换:

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")//等价于strconv.ParseInt("123",10,0)也就是转换为int类型
fmt.Println(n2) // 123

n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax

无论是数字转数字字符串还是数字字符串转数字都需要可兼容,否则就会报错,就如上把"AAA"转为int就报错了。

猜数字游戏简单案例

该案例取自字节跳动青训营Go原理基础课程,游戏规则很简单,程序随机生成一个数,输入一个数判断猜大了还是猜小了,下面看一下完整实现(附带一些我自己的注释来解释):

func main() {
   maxNum := 100//此处为定义随机数的最大范围
   rand.Seed(time.Now().UnixNano())//这个地方使用rand的Seed设置一个初始种子即初始化
   secretNumber := rand.Intn(maxNum)//生成一个随机数,范围为[0,maxNum],maxNum必须大于0
   
   fmt.Println("请输入你猜的数字")
   reader := bufio.NewReader(os.Stdin)//此处为生成一个阅读器读取控制台输入的数据,其样式作用和java的Scaner(System.in)差不多。
   for {
      input, err := reader.ReadString('\n')//读取输入数据包括换行符
      if err != nil {
         fmt.Println("输入有误,请再次输入", err)
         continue
      }
      input = strings.Trim(input, "\r\n")//去掉换行符

      guess, err := strconv.Atoi(input)//将数据由数字字符串转为整数
      if err != nil {
         fmt.Println("无效输入,请重新输入")
         continue
      }
      fmt.Println("你猜的是", guess)
      //此处进行比较
      if guess > secretNumber {
         fmt.Println("你猜大了,请再次输入")
      } else if guess < secretNumber {
         fmt.Println("你猜小了,请再次输入")
      } else {
         fmt.Println("恭喜,你猜对了!")
         break //结束循环
      }
   }
}

这里简单说一下,据我所知,rand的随机是伪随机,至少java里面是这样的不知道go优化没有,想要实现一个真随机的话要用到一个随机模型和随机算法,感兴趣的朋友可以自行百度。

小结

自此,我已经学会了go的常用基础语法并实现一个简单的实战案例,在笔耕的过程中也是对我自己的巩固和一种提升,希望大家一起进步,多多讨论并提出不足。

参考