Golang基础语法上手 | 青训营笔记

130 阅读4分钟

Golang基础语法上手

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。

本篇文章主要是记录了老师上课所讲golang基础语法入门的几个关键要点,在手撸这些代码并调试以及根据老师的讲解之后总结出的一些个人对语法上的关键点和注意事项的提要。以及包含了猜数字项目的个人代码实现,分别是字符串处理版本和简易输入输出版本。

从Hello World开始

最简单的Hello World代码,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,像 fmt、os、io 等这样具有常用功能的内置包在 Go语言中有 150 个以上,它们被称为标准库,大部分(一些底层的除外)内置于 Go 本身。

package main
​
import "fmt"func main() {
    fmt.Println("Hello World!")
}
/*
运行结果:
Hello World!
*/

golang中的import用法和C++中的include一样,用于拉取标准库

var的用法

package main
​
import (
    "fmt"
    "math"
)
​
func main() {
    var a = "initial"
    var b, c int = 1, 2
    var d = true
    var e float64 //未被初始化的变量值为0
    f := float32(e)
​
    g := a + "foo"
    fmt.Println(a, b, c, d, e, f) //initial 1 2 true 0 0
    fmt.Println(g)
​
    const s string = "constant"
    const h = 500000000
    const i = 3e20 / h
    fmt.Println(s, h, i, math.Sin(h), math.Sin((i)))
}
​

golang声明定义对象的方式用两种

  • var开头的经典定义方式

  • := 快捷初始化的方式

    而var打头的经典定义方式又分为以下几种

  • 声明对象不定义,必须指明类型

    var a int;

  • 声明对象并定义,可以省略类型

    var a int =5

    var flag = true

const的使用法和var相同,都需要打头。但又和C++中的常量修饰符不同,接下来我们做一组对比

const int x = 12; //c++
const hx int = 12; //golangconst string str = "abc"; //C++
const hstr = "abc"        //GOLANG
const hstr string = "abc"    //GOLANG

细微之处大家可以自行体会,在golang中,实际的类型要写在变量名之后,写在变量名之前的是显示定义常量/变量的关键字。

if-else

语法注意点

  • if后面不需要带括号
  • if后面必须跟大括号,不能像C++当中一样直接跟上一条语句
package main
​
import "fmt"func main() {
    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")
    }
}

for语句

package main
​
import "fmt"func main() {
​
    i := 1
    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)
    }
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }
}

golang没有while 想要达到while的效果就必须要用for循环

  • for 后面直接跟大括号就可以达到死循环的效果
  • 可以使用经典的C风格三段式
  • 循环中可以只带条件判断语句,这种写法和C++中的经典while写法一样
  • 可以在for循环语句中使用break和continue

Switch分支结构

golang的Switch写法和C++大体上是一样的,但是有几个注意细节

  • switch后面的值判断语句不要加括号
  • 每一条case后面不需要加上break ,默认跑完一条case的执行语句之后就终止了
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")
    case 4, 5:
        fmt.Println("four or five")
    default:
        fmt.Println("other")
    }
​
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }
}

数组

package mainimport "fmt"
​
func main() {
    var a [5]int
    a[4] = 100
​
    fmt.Println("get:", a[2])
    fmt.Println("len:", len(a))
​
    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println(b)
​
    var towD [2][3]int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            // fmt.Println(tow)
            towD[i][j] = i + j
        }
    }
    fmt.Println("2d : ", towD)
}
/*
运行结果:
get: 0
len: 5
[1 2 3 4 5]
2d :  [[0 1 2] [1 2 3]]
*/

注意声明格式 先写数组规模再写数组类型

var a [5]int

切片(slice)

package main

import "fmt"

func main() {

	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

	s = append(s, "d")
	s = append(s, "e", "f")
	fmt.Println(s) // [a b c d e f]

	c := make([]string, len(s))
	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]
}

切片是一种可变长度的数组,可以任意更改它的长度,也有更多丰富的操作:

s:=make([]string,3) 	//创建一个切片,初始大小为3
//...
s:= append(s,"d") 		//append操作往切片中追加元素
copy(c,s)				//使用copy函数来拷贝数据,把s中的数据拷贝到c中
s[2:5]					//三个元素 2\3\4下表的元素

slice和C++vector一样,会扩容,并且是以乘以二的机制增加,和vector一样。

golang中的slice和python有所不同,golang中的切片不支持负数

map

package main

import "fmt"

func main() {
	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

	r, ok := m["unknow"]
	fmt.Println(r, ok) // 0 false

	delete(m, "one")

	m2 := map[string]int{"one": 1, "two": 2}
	var m3 = map[string]int{"one": 1, "two": 2}
	fmt.Println(m2, m3)
}

map就是哈希表或者字典,他的具体操作如下

make(map[string]int) 			//key-string value-int
r, ok := m["unknow"]			//这里的ok可以用于判断此key值是否存在

使用中括号写法添加kv对,delete删除kv对。

值得注意的是,map中元素的顺序是完全无序的,不会按照字典序排序

range

package main

import "fmt"

func main() {
	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
	}
}

这种写法就有点类似于C++中的冒号写法

for(auto it : container){
	//...
}

func函数

package main

import "fmt"

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
}

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
}	

在GO中返回值可以为多个,在一般的业务当中,第一个值往往代表了真正的返回值,第二个值是错误判断信息

func exists(m map[string]string, k string) (v string, ok bool) { //这个函数返回map对key值的查找 第二个值用于key是否重组奶
	v, ok = m[k]
	return v, ok
}

指针

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
}

同C++

结构体

package main

import "fmt"

type user struct {
	name     string
	password string
}

func main() {
	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"

	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
}

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

func checkPassword2(u *user, password string) bool {
	return u.password == password
}

结构体方法

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
}

结构体方法就类似于成员函数。他的写法是在普通函数前面加上类的名称以及对象名,从而把一个普通函数更改为一个成员函数

错误处理

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("not found")
}

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用以表示此函数可能会产生错误。因此这个函数就必须返回两个参数,第二个返回参数代表了错误信息。

func findUser(users []user, name string) (v *user, err error) { 
}

字符串操作

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
	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
}
  • strings.Contains(a,param) 用于判断param是否存在于a中
  • strings.Count(a,b) 计算b在a中的出现次数
  • strings.HasPrefix(a,prefix) 判断a是否有prefix前缀
  • strings.HasSuffix(a.suffix) 判断a是否有suffix后缀
  • strings.Index(a,str) 定位str字符串在a中第一次出现的下标
  • strings.Join([]string{"a" , "b","c"} , "-") 在每两个字符串中间加入字符“-" 结果字符串是 a-b-c
  • strings.Repeat(a,times) 返回字符串a重复times次之后的结果 hello*times
  • strings.Replace(a,"e","E",-1) 替换a中字符串的e为E hEllo
  • strings.ToLower(str)
  • strings.ToUpper(str)

字符串格式化

package main

import "fmt"

type point struct {
	x, y int
}

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
}

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)		//把一个结构体转化为json字符串 这个操作称之为序列化
	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"}}
}

保正结构体的每一个变量的首字母都大写 这样这个结构体就可以序列化 。如果希望输出的属性名是小写,则需要在结构体变量之后加上字符串'json:age'以标明

type userInfo struct {
	Name  string
	Age   int `json:"age"`
	Hobby []string
}
	buf, err := json.Marshal(a)		//把一个结构体转化为json字符串 这个操作称之为序列化

如果不加string打印出来的都是十六进制的数字

	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

同样的我们也可以对json类型的变量进行反序列化

	err = json.Unmarshal(buf, &b)

事件处理

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
}

最常用的是time.now()用于获取时间

利用Format()方法来格式化时间字符串

	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36

可以通过time.Unix()来获取时间戳

数字解析

用于进行字符串和数组之间的转换

package main

import (
	"fmt"
	"strconv"
)

func main() {
	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
}

ParseInt()这个函数有三个参数,

  • 第二个参数表示字串的进制,
  • 第三个参数表示返回的是一个64位进度的数

我们也可以使用 strconv.Atio("123")来快速的把一个字符串转化为一个数字

	n, _ = strconv.ParseInt("0x1000", 0, 64)

进程信息

最后我们来看一下golang中获取进程相关的信息:

  • os.Args来获得命令行的相关参数
  • os.Getenv("PATH") 用来获取环境变量
  • os.Setenv("AA","BB") 用来设置环境变量
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
}

猜数字

给出两种写法

简单写法

输入值都简化了,简单易懂

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	var max_num int
	fmt.Println("请输入最大值")
	fmt.Scanf("%v", &max_num)

	// reader = rand.Reader()
	secert_number := rand.Intn(max_num)
	fmt.Println("The secert number is", secert_number)
	for {
		var number int
		fmt.Scanf("%v", &number)

		if number > secert_number {
			fmt.Println("Your input is bigger than the secert number,try again")
		} else if number < secert_number {
			fmt.Println("Your input is smaller than the secert number,try again")
		} else {
			fmt.Println("Yes! Congratulations!")
			break
		}
	}
}

字符串处理写法

本写法和官方写法v5不同之处在于,最大值由键盘给出,并且将其当作字符串进行处理

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	max_num, _ := reader.ReadString('\n')

	max_num = strings.TrimSuffix(max_num, "\n") //ReadString输入的字符串需要去掉尾椎
	max_num_real, _ := strconv.Atoi(max_num)

	// max_num := 100
	rand.Seed(time.Now().UnixNano())
	secert_number := rand.Intn(max_num_real) //随机给一个值
	fmt.Println(secert_number)
	fmt.Println("Please enter your guess")
	// reader = bufio.NewReader(os.Stdin)

	for {
		number_str, _ := reader.ReadString('\n')
		number_str = strings.TrimSuffix(number_str, "\n")
		number, err := strconv.Atoi(number_str)
		if err != nil {
			fmt.Println("ATOI ERROR")
			return
		}
		if number == secert_number {
			fmt.Println("YES ! CONG!")
			return
		} else if number > secert_number {
			fmt.Println("BIGGER")
		} else {
			fmt.Println("SMALLER")
		}

	}
}

这个地方需要注意几点

创建一个reader读取器

reader := bufio.NewReader(os.Stdin)

约定读取器读取字符串,并遇到换行符作为结尾

max_num, _ := reader.ReadString('\n')

需要去掉读取到的换行符,否则在Atoi转的时候会报错

max_num = strings.TrimSuffix(max_num, "\n") //去掉字符串尾部的\n字符

//TrimSuffix的源码实现
// TrimSuffix returns s without the provided trailing suffix string.
// If s doesn't end with suffix, s is returned unchanged.
func TrimSuffix(s, suffix string) string {
	if HasSuffix(s, suffix) {
		return s[:len(s)-len(suffix)]
	}
	return s
}

给一个随机值,并给出上界

secert_number := rand.Intn(max_num_real) //随机给一个值

官方写法

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
	maxNum := 100
	rand.Seed(time.Now().UnixNano())
	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
		}
	}
}

总结与思考

本人起初是有一些golang基础的,2021年暑假在和发小去无锡实习时就已经接触过golang,算是老熟人了,一回生二回熟三会都该认朋友,此次趁着青训营这个机会来系统的学习一下后端架构知识以及golang这门语言了。

在经过这一天的高强度压缩式的学习golang快速上手一门语言的方法就是弄清楚它的每一个经典样例,去动手实践,去调试,宁愿多走一些弯路,多绕一些原路,走过所有的坑,以后将无坑可走。

(除非它是像C++一样复杂且博大精深的一门语言,C++有太多的隐藏特性等待着开发者去挖掘,需要开发者沉下心来阅读大师们的作品...