Go 语言入门指南:基础语法和常用特性解析 | 青训营

149 阅读12分钟

Go 语言入门指南:基础语法和常用特性解析

前言

面向用户

  • 有C、C++或Java基础

笔者环境

  • macos 10.15.7
  • Golang 1.18
  • GoLand 2022.01

读完本文可以获得

  • 掌握Go的安装和配置
  • 掌握Go的基本语法
  • 熟悉Go的常用特性

一、简介&安装

介绍

Golang(下文简称Go)语言是一种高性能、高并发的语言,语法简单且拥有丰富的标准库以及完善的工具链,同时可以实现跨平台、快速编译和垃圾自动回收。

安装

1. 下载&安装

通过浏览器下载对应系统版本的GO安装包,并根据导引安装

(下列链接任选其一进行下载)

2. 配置环境变量

go的安装过程默认会将GOROOT/bin目录加入到PATH(export PATH=$PATH:/usr/local/go/bin),这使得我们可以在控制台的任意路径使用go命令。

我们仍需要配置几个Go的环境变量,了让Go和第三方工具能够正确地找到源代码、编译器、库文件、可执行文件的位置。

环境变量
  1. GOROOT

    • GOROOT 是 Go 的安装根目录,需要在环境变量中手动明确指定。

    • Go命令需要依赖这个变量设置来工作。第三方工具(如IDE)也需要知道GOROOT的路径

  2. GOPATH

    GOPATH是你的代码仓库所在的位置。go tool会在这个路径下查找和安装代码包。

    GOPATH 是用来存放你的 Go 项目的工作目录,你的代码、依赖包、测试文件等都会放在这个目录下。如果你使用了 Go modules,那么 GOPATH 就不是必须的了,你可以在任意目录下开发你的代码,Go 会自动管理你的依赖包。但是如果你没有使用 Go modules,或者你想使用一些第三方的工具,比如 go get,那么 GOPATH 还是需要配置的。

  3. GOBIN

    • GOBIN是用来指定可执行文件的安装目录的

    • GOBIN默认路径是空的,这意味着当你使用go install命令编译和安装可执行文件时,它们会被放在GOPATH/bin目录中,但有可能当前用户对GOPATH/bin目录没有写权限,因此执行go install命令会出现访问拒绝。

    • 通过设置GOBIN的值可以自定义到有写权限的目录。

    • 同时将GOBIN添加到系统PATH,可以在任意目录下执行GOBIN目录下的可执行文件,而不用输入完整路径。

  4. GO111MODULE

    • GO111MODULE是一个环境变量,它用来控制Go的模块功能。
    • 当它的值为on时,表示启用模块支持,这样Go会忽略GOPATH和vendor目录,只根据go.mod文件来管理依赖。
    • go.mod文件是一个文本文件,它记录了当前模块的名称、版本和依赖的其他模块。
  5. GOPROXY

    • GOPROXY是一个环境变量,它用来指定Go模块的代理服务器。

    • 当你使用go命令获取、编译或运行依赖的模块时,Go会通过GOPROXY指定的代理服务器来下载模块,而不是直接从源码仓库下载。这样可以加速模块的获取,也可以避免一些网络问题或源码仓库的限制。

    • 国内代理服务器

      # 配置 GOPROXY 环境变量,以下三选一
      # 1. 七牛 CDN
      export GOPROXY=https://goproxy.cn,direct
       
      # 2. 阿里云
      export GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
       
      # 3. 官方
      export GOPROXY=https://goproxy.io,direct
      
      # 查看当前环境变量配置
      go env | grep GOPROXY
      
操作步骤
  1. 进入terminal,输入go env查看Go的环境变量信息

    spaceqi@macbookpro ~ % go env
    GO111MODULE="on"
    GOARCH="amd64"
    GOBIN=""
    GOCACHE="/Users/spaceqi/Library/Caches/go-build"
    GOENV="/Users/spaceqi/Library/Application Support/go/env"
    GOEXE=""
    GOEXPERIMENT=""
    GOFLAGS=""
    GOHOSTARCH="amd64"
    GOHOSTOS="darwin"
    GOINSECURE=""
    GOMODCACHE="/Users/spaceqi/go/pkg/mod"
    GONOPROXY=""
    GONOSUMDB=""
    GOOS="darwin"
    GOPATH="/Users/spaceqi/go"
    GOPRIVATE=""
    GOPROXY="https://goproxy.cn,direct"
    GOROOT="/usr/local/go"
    ...
    
  2. 参数设置

    把配置写到profile文件,实现每次登录自动加载环境。

    ~/.bash_profile 文件用于设置环境变量等配置信息,每次打开终端时都会自动加载

    vi ~/.bash_profile 
    
    export GOROOT=/usr/local/go # 默认
    export GOPATH=/Users/spaceqi/go # 你的工作区,/Users/spaceqi该目录下具有root权限
    export GOBIN=$GOPATH/bin # 重定向可执行文件
    export PATH=$GOBIN:$PATH # 加入系统PATH,便于访问GOBIN目录下的可执行文件
    export GO111MODULE=on # 启用Go Modules 功能
    export GOPROXY=https://goproxy.cn # 使用国内代理
    
    #退出文件后,并重新加载文件
    source ~/.bash_profile 
    

开发工具

  • VSCode编辑器(需要添加插件)
  • Golang(IDE)

二、基础语法

2.1 输出

package main

import "fmt"

func main() {
	fmt.Println("hello word!")
}

  • package main main包是程序的入口包
  • import "fmt" 导入输入输出包,用于向屏幕输入输出字符串,格式化字符串
  • fmt.Println("hello word!") 向控制台输出内容

2.2 变量

常见数据类型

  • 整数
  • 浮点
  • 字符串
  • 布尔

变量声明

// 方式1 var name [type]=value, type类型一般由编译器自动推导
var nickname string="samuelZhang"
var age=20

// 方式2 name:=value
gender:="male"
flag:=false

常量声明

const name=value
// 常量没有确定的类型,会根据上下午自动推断

运算符&优先级

与C和C++类似,不同点在于

  • Go没有前缀自增运算符(++i)

2.3 if-else

与C和C++类似,不同的点在于

  • 分支条件不需要使用小括号包裹,手动写上小括号,也会被编译器自动去掉
  • if后面必须跟大括号
package main

import "fmt"

func main() {
	score := 87

	if score >= 90 {
		fmt.Println("excellent!")
	} else if score >= 80 {
		fmt.Println("good")
	} else if score >= 70 {
		fmt.Println("pass")
	} else {
		fmt.Println("fail")
	}
}

2.4 循环

与C和C++类似,不同的点在于

  • 循环条件不需要使用小括号包裹,手动写上小括号,也会被编译器自动去掉
  • for后面必须跟大括号
  • 没有while、do-while循环
package main

import "fmt"

func main() {
	sum := 0
	for i := 1; i <= 100; i++ {
		sum += i
	}
	fmt.Println(sum)
}

2.5 Switch

与C和C++类似,不同的点在于

  • Switch 条件 不需要手动写上小括号,手动写上小括号,也会被编译器自动去掉
  • case中没有break,当一个case语句执行完后,会自动跳出当前switch
  • case的表达式可以是任意类型
package main

import (
	"fmt"
)

func main() {
	grade := 5

	switch grade {
	case 1:
		fmt.Println("大众")
	case 2:
		fmt.Println("黄金")
	case 3:
		fmt.Println("铂金")
	case 4, 5, 6:
		fmt.Println("钻石")
	default:
		fmt.Println("异常")
	}
}

2.6 数组

与C和C++类似,数组是一个连续序列。

在实际开发中很少使用数组,因为是固定长度的,通常使用切片。

package main

import (
	"fmt"
)

func main() {
	var name [5]string
	name[2] = "samuelZhang"
	fmt.Println(name, name[2], len(name)) // [  samuelZhang  ] samuelZhang 5

	score := [5]int{60, 70, 80, 90, 100}
	fmt.Println(score) // [60 70 80 90 100]

	var twoD [2][3]int
	for i := 0; i < 2; i++ {
		for j := 0; j < 3; j++ {
			twoD[i][j] = i + j
		}
	}
	fmt.Println(twoD)	// [[0 1 2] [1 2 3]]

}

2.7 切片

切片(slice)不同于数组,它可以任意更改长度。

通过make可以创建一个切片

  • 可以像数组一样取值和赋值
  • 也可以使用append进行追加,当容量不够后,会进行扩容,并返回一个新的切片。
  • 可以像Python一样进行切片操作,但不支持负索引
package main

import (
	"fmt"
)

func main() {
	var name = make([]string, 2) // 创建一个长度为2的切片
	name[0] = "samuelZhang"
	name[1] = "alexWang"
	fmt.Println(name, len(name)) // [samuelZhang alexWang] 2
	name = append(name, "tonyBao", "amyChen")
	fmt.Println(name, len(name)) //	[samuelZhang alexWang tonyBao amyChen] 4
	fmt.Println(name[1:4])       // [alexWang tonyBao amyChen]
}

2.8 map

map类似Python里的字典和Java里的Hashmap。

Go中的map是无序的(插入、遍历都是随机顺序)

package main

import (
	"fmt"
)

func main() {
	var score = make(map[string]int) // 使用make创建一个map
	score["math"] = 99
	score["english"] = 97
	score["writing"] = 90
	fmt.Println(score, score["math"], len(score)) // map[english:97 math:99 writing:90] 99 3

	r, ok := score["reading"] // 声明r,ok两个变量并初始化,r是score["reading"]的值,如果key存在,ok为true,否则为false
	fmt.Println(r, ok)        // 0 false
	r, ok = score["writing"]  // 为r,ok两个变量赋值
	fmt.Println(r, ok)        // 90 true

	goods := map[string]string{"brand": "apple", "Model": "X"} // 声明一个map并初始化
	goods["price"] = "$999"
	fmt.Println(goods, len(goods)) // map[Model:X brand:apple price:$999] 3

}

使用make创建的map和直接使用map字面量创建map的主要区别:

  • 使用make: 返回的是指向map的指针
  • 使用字面量:返回的是map的值

2.9 range

  • 通过使用range来对切片(数组)、map进行遍历
    • 对于切片,range会返回两个值,索引和对应位置的值
    • 对于map,range会返回两个值,key和对应的value
    • 如果不需要某个值,使用下划线“_”进行忽略
package main

import (
	"fmt"
)

func main() {

	var brand [2]string
	brand[0] = "apple"
	brand[1] = "huawei"
	for index, value := range brand {
		fmt.Println("index:", index, ",value:", value)
		// index: 0 ,value: apple
		// index: 1 ,value: huawei
	}

	temperature := make([]float64, 5)
	copy(temperature, []float64{26, 29.5, 35, 37, 39.9, 41.5}[:]) // 用copy函数把数组复制到切片中
	for _, value := range temperature {
		fmt.Println("value:", value)
		// value: 26
		// value: 29.5
		// value: 35
		// value: 37
		// value: 39.9
	}

	var score = make(map[string]int)
	score["math"] = 99
	score["english"] = 97
	score["writing"] = 90

	for key, value := range score {
		fmt.Println("key:", key, ",value:", value)
		// key: math ,value: 99
		// key: english ,value: 97
		// key: writing ,value: 90
	}
	goods := map[string]string{"brand": "apple", "Model": "X"}
	for _, value := range goods {
		fmt.Println("value:", value)
		// value: apple
		// value: X
	}

}

2.10 函数

Go的函数和C和Python的函数使用起来类似,不同点在于

  • 函数的定义,方法参数类型和返回值类型都后置

    func name(param1 tyep,parm2 type) (returnTYpe){
    	return ...
    }
    
  • 函数支持返回多个值,通常返回两个(1个是结果,一个是错误信息)

package main

import (
	"fmt"
)

func sum(t []float64) float64 {
	res := 0.0
	for i := 0; i < len(t); i++ {
		res += t[i]
	}
	return res / float64(len(t))
}

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

func main() {

	temperature := make([]float64, 5)
	copy(temperature, []float64{26, 29.5, 35, 37, 39.9, 41.5}[:])
	fmt.Println(sum(temperature))

	goods := map[string]string{"brand": "apple", "Model": "X"}
	v, ok := exists(goods, "brand")
	fmt.Println(v, ok) // apple true
	v, ok = exists(goods, "price")
	fmt.Println(v, ok) // false
}

2.11 指针

Go的指针和C和C++的指针使用起来类似,不同点在于

  • Go的指针操作有限,主要用途是对传入的参数进行修改
package main

import (
	"fmt"
)

func swap0(a int, b int) {
	a, b = b, a
}

func swap(a *int, b *int) {
	*a, *b = *b, *a
}

func main() {
	a := 1
	b := 5
	fmt.Println(a, b) // 1 5
	swap0(a, b)
	fmt.Println(a, b) // 1 5

	fmt.Println("============")
	fmt.Println(a, b) // 1 5
	swap(&a, &b)
	fmt.Println(a, b) // 5 1
}

2.12 结构体

Go的指针和C的结构体使用起来类似

package main

import (
	"fmt"
)

type user struct {
	id     string
	name   string
	gender string
	age    int
}

func find(u user, id string) (user, bool) {
	if u.id == id {
		return u, true
	} else {
		return user{}, false
	}
}

func updateAge(u *user, id string, age int) {
	if u.id == id {
		u.age = age
	}
}

func main() {
	samuel := user{id: "1001", name: "samuelZhang", gender: "Male"}
	samuel.age = 20
	fmt.Println(samuel) // {1001 samuelZhang Male 20}

	chen := user{}    // 表示一个空的user值,它的所有字段都是零值
	fmt.Println(chen) // {    0}

	alex := user{"1002", "alexWang", "Male", 19} // 如果不显示指定字段名,需要传入所有的字段进行初始化
	fmt.Println(alex)                            // {1002 alexWang Male 19}

	u, ok := find(samuel, "1001")
	fmt.Println(u, ok) // {1001 samuelZhang Male 20} true

	u, ok = find(samuel, "1002")
	fmt.Println(u, ok) // {    0} false

	updateAge(&samuel, "1001", 21)
	fmt.Println(samuel) // {1001 samuelZhang Male 21}

}

2.13 结构体方法

Go的结构体方法和其他面向对象语言的的成员函数使用起来类似,不同点在于

Go的结构体方法在普通方法上的方法名前加上结构体变量

func (name,nameStruct) name(param1 tyep,parm2 type) returnTYpe{
	return ...
}
package main

import (
	"fmt"
)

type user struct {
	id     string
	name   string
	gender string
	age    int
}

func (u user) find(id string) bool {
	if u.id == id {
		return true
	} else {
		return false
	}
}

func (u *user) updateAge(age int) {
	u.age = age
}

func main() {
	samuel := user{id: "1001", name: "samuelZhang", gender: "Male"}
	samuel.age = 20
	fmt.Println(samuel) // {1001 samuelZhang Male 20}

	ok := samuel.find("1001")
	fmt.Println(ok) // true

	ok = samuel.find("1002")
	fmt.Println(ok) // false

	samuel.updateAge(21)
	fmt.Println(samuel) // {1001 samuelZhang Male 21}

}

2.14 错误处理

Go的错误处理使用error对象,常用于函数的第二个返回值。

  • 在函数实现的时候,如果出现错误,就可以返回nil和error,如果没有的话就返回结果和nil
  • 调用端通过if-else就可以对错误进行处理
package main

import (
	"errors"
	"fmt"
)

type user struct {
	id     string
	name   string
	gender string
	age    int
}

func find(users []user, id string) (*user, error) {
	for _, v := range users {
		if v.id == id {
			return &v, nil
		}
	}
	return nil, errors.New("No find user")
}

func main() {
	samuel := user{id: "1001", name: "samuelZhang", gender: "Male"}
	samuel.age = 20
	fmt.Println(samuel) // {1001 samuelZhang Male 20}

	alex := user{"1002", "alexWang", "Male", 19}

	users := make([]user, 5)
	users[0] = samuel
	users[1] = alex
	fmt.Println(users) // [{1001 samuelZhang Male 20} {1002 alexWang Male 19} {   0} {   0} {   0}]

	v, err := find(users, "1001")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(v.name) // samuelZhang

	v, err = find(users, "1003")
	if err != nil {
		fmt.Println(err) // // No find user
		return
	}
	fmt.Println(v.name)

}

2.15 字符串操作

Go语言的strings标准库有非常多的字符串操作,如join,count,index,replace等

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

}

2.16 字符串格式化

Go的字符串格式化和C的字符串格式化使用起来类似,不同点在于

  • 使用%v来打印任意变量,
  • 使用%+v来打印详细变量的结果
  • 使用%#v来打印更详细变量的结果
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
}

2.17 JSON处理

Go语言中的JSON操作非常简单

  • json.Marshal可以将结构体中以大写字母开头的字段进行序列化
  • json.MarshalIndent可以将结构体中以大写字母开头的字段进行序列化,并且指定前缀和缩进来美化输出
  • json.Unmarshal可以将一个JSON格式的字节数组反序列化成结构体变量
package main

import (
	"encoding/json"
	"fmt"
)

type user struct {
	id     string
	Name   string
	Gender string
	Age    int
	Hobby  []string
}

func main() {
	samuel := user{id: "1001", Name: "samuelZhang", Gender: "Male"}
	samuel.Age = 20
	samuel.Hobby = make([]string, 2)
	copy(samuel.Hobby, []string{"Jogging", "snorkeling"})
	fmt.Println(samuel) // {1001 samuelZhang Male 20 [Jogging snorkeling]}

	buf, err := json.Marshal(samuel) // [序列化]将samuel转换为JSON格式的字节数组,并存储在buf中。
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97 109 101 34 58 ...]
	fmt.Println(string(buf)) // {"Name":"samuelZhang","Gender":"Male","Age":20,"Hobby":["Jogging","snorkeling"]}

	buf, err = json.MarshalIndent(samuel, "", "\t") // [序列化]将samuel转换为JSON格式的字节数组且指定前缀和缩进来美化输出,并存储在buf中。
	if err != nil {
		panic(err)
	}
	fmt.Println(buf) // [123 10 9 34 78 97 109 101 34 58  ...]
	fmt.Println(string(buf))
	// {
	// 	"Name": "samuelZhang",
	// 	"Gender": "Male",
	// 	"Age": 20,
	// 	"Hobby": [
	// 		"Jogging",
	// 		"snorkeling"
	// 	]
	// }

	var toSamuel user
	err = json.Unmarshal(buf, &toSamuel) // [反序列化]把一个JSON格式的字节数组buf转换为user类型的变量toSamuel,并存储在toSamuel的地址中
	if err != nil {
		panic(err)
	}
	fmt.Printf("toSamuel: %v\n", toSamuel) // toSamuel: { samuelZhang Male 20 [Jogging snorkeling]}

}

2.18 时间处理

Go语言的time标准库有非常多的日期和时间操作,如对日期进行处理,例如获取当前时间、格式化时间、解析时间、计算时间差等。

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println(now) // 2023-07-26 17:43:44.778416 +0800 CST m=+0.000124636
	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()) // 1690364624
}

2.19 数字解析

Go语言中提供strconv标准库对字符串和数字进行相互转换,例如strconv.Atoi()、strconv.Itoa()、strconv.ParseInt()、strconv.FormatInt()等函数。你可以使用这些函数来处理不同进制和位数的数字,以及不同格式的字符串。

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

	n3 := strconv.FormatInt(1024, 16)
	fmt.Println(n3) // 400
}

2.20 环境变量

Go语言中提供os和os/exec包是Go语言中的两个标准库,它们提供了操作系统相关的功能接口,例如文件、环境变量、进程、命令等。

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	// go run main.go a b c d
	fmt.Println(os.Args)               // 显示命令行参数,包括程序名和传入的参数a b c d [/var/folders/qk/lq98x2wj1rl6vlghzvwms_h40000gn/T/go-build1407077754/b001/exe/main.go a b c d]
	fmt.Println(os.Getenv("PATH"))     // 显示环境变量PATH的值,即可执行文件的搜索路径 /usr/local/go/bin...
	fmt.Println(os.Setenv("AA", "BB")) // 设置环境变量AA的值为BB,并返回nil表示成功

	buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput() // 创建一个命令对象,用于在/etc/hosts文件中查找127.0.0.1这个字符串
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf)) // 127.0.0.1       localhost
}

三、常用特性解析

  1. goroutine - 轻量级线程支持大规模的并发处理。
  2. channel - goroutine间的通信和数据传递渠道,支持基于CSP的并发。
  3. interface - 接口,提供了非常灵活的组件解耦能力。
  4. struct - 轻量可组合的数据结构,是对面向对象编程的支持。
  5. range - 遍历数据结构的便捷方式,可以用于数组、切片、map、channel等。
  6. select - 监听channel操作的选择器,支持非阻塞的channel通信。
  7. defer - 函数退出前执行代码的机制,常用于释放资源。
  8. error - 用error类型表示错误对象,与异常有区别。
  9. slice - 动态数组切片,灵活传递和处理序列数据。
  10. map - 快速查找的哈希表实现,支持各种键值对数据。