Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI 刷题

36 阅读15分钟

开始

在开始之前,先介绍下官方文档以及如何利用官方提供的工具,如果能够妥善使用官方提供的便利就能提升学习的效率以减少不必要的时间浪费。本文很多内容也来自于官方网站。

官方文档

地址:Documentation - The Go Programming Languageopen in new window

文档里有着对于学习Go语言所需要准备的一切东西,包括安装,快速开始,代码示例,风格建议,以及许多在线教程,大多数都是全英文的,少数支持中文,不过并没有什么特别晦涩难懂的词汇,大致意思都比较容易看懂。

Go之旅

地址:Go 语言之旅 (go-zh.org)open in new window

这是由官方编写的一个非常简洁明了的教程,全中文支持,通过互动式的代码教学来帮助你快速了解Go语言的语法与特性,适合想要快速了解Go语言的人,如果将该教程浏览过一遍后,那么本站的基础教程理解起来会轻松很多。

参考手册

地址:The Go Programming Language Specificationopen in new window

参考手册的重要性不言而喻,参考手册的内容永远会随着版本的变化而变化,时刻保持最新,其内容有:词法结构,概念定义,语句定义等等,这是一些关于Go语言中最基础的定义,适合有需要的时候查询一些概念,同时里面也有着不少的代码示例。

语法基础

基本语法

Go的基本语法十分简洁且简单,下面通过一个简单且经典的示例来进行讲解。

package main

import "fmt"

func main() {
   fmt.Println("Hello world!")
}

package关键字代表的是当前go文件属于哪一个包,启动文件通常是main包,启动函数是main函数,在自定义包和函数时命名应当尽量避免与之重复。包的命名风格建议都是小写字母,并且要尽量简短。

import是导入关键字,后面跟着的是被导入的包名。

func是函数声明关键字,用于声明一个函数。

fmt.Println("Hello world!")是一个语句,调用了fmt包下的Println函数进行控制台输出。

标识符

标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:

  • 只能由字母,数字,下划线组成
  • 只能以字母和下划线开头
  • 严格区分大小写
  • 不能与任何已存在的标识符重复,即包内唯一的存在
  • 不能与Go任何内置的关键字冲突

下方列出所有的内置关键字,也可以前往参考手册-标识符open in new window查看更多细节

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

运算符

下面是Go语言中支持的运算符号的优先级排列,也可以前往参考手册-运算符open in new window查看更多细节。

Precedence    Operator
    5             *  /  %  <<  >>  &  &^
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||

提示

Go语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++++i这样的问题。

a++ // 正确
++a // 错误
a-- // 正确

还有一点就是,它们不再具有返回值,因此a = b++这类语句的写法是错误的。

变量

声明

在go中的类型声明是后置的,变量的声明会用到var关键字,格式为var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则。

var intNum int
var str string
var char byte

当要声明多个相同类型的变量时,可以只写一次类型

var numA, numB, numC int

当要声明多个不同类型的变量时,可以使用()进行包裹,可以存在多个()

var (
	name    string
	age     int
	address string
)

var (
	school string
	class int
) 

一个变量如果只是声明而不赋值,那么变量存储的值就是对应类型的零值。

赋值

赋值会用到运算符=,例如

var name string
name = "jack"

也可以声明的时候直接赋值

var name string = "jack"

或者这样也可以

var name string
var age int
name, age = "jack", 1

第二种方式每次都要指定类型,可以使用官方提供的语法糖:短变量初始化,可以省略掉var关键字和后置类型,具体是什么类型交给编译器自行推断。

name := "jack" // 字符串类型的变量。

虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种代码无法通过编译。

a := 1
a = "1"

还需要注意的是,短变量初始化不能使用nil,因为nil不属于任何类型,编译器无法推断其类型。

name := nil // 无法通过编译

短变量声明可以批量初始化

name, age := "jack", 1

短变量声明方式无法对一个已存在的变量使用,比如

// 错误示例
var a int
a := 1

// 错误示例
a := 1
a := 2

但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如

a := 1
a, b := 2, 2

这种代码是可以通过编译的,变量a被重新赋值,而b是新声明的。

在go语言中,有一个规则,那就是所有在函数中的变量都必须要被使用,比如下面的代码只是声明了变量,但没有使用它

func main() {
	a := 1
}

那么在编译时就会报错,提示你这个变量声明了但没有使用

a declared and not used

这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译。

var a = 1

func main() {
	
}

流程控制

条件控制

if else

if else至多两个判断分支,语句格式如下

if expression {

}

或者

if expression {

}else {

}

expression必须是一个布尔表达式,即结果要么为真要么为假,必须是一个布尔值,例子如下:

func main() {
   a, b := 1, 2
   if a > b {
      b++
   } else {
      a++
   }
}

switch

switch语句也是一种多分支的判断语句,语句格式如下:

switch expr {
	case case1:
		statement1
	case case2:
		statement2
	default:
		default statement
}

一个简单的例子如下

func main() {
   str := "a"
   switch str {
   case "a":
      str += "a"
      str += "c"
   case "b":
      str += "bb"
      str += "aaaa"
   default: // 当所有case都不匹配后,就会执行default分支
      str += "CCCC"
   }
   fmt.Println(str)
}

循环控制

在Go中,有仅有一种循环语句:for,Go抛弃了while语句,for语句可以被当作while来使用。

for

语句格式如下

for init statement; expression; post statement {
	execute statement
}

当只保留循环条件时,就变成了while

for expression {
	execute statement
}

这是一个死循环,永远也不会退出

for {
	execute statement
}

切片

切片在Go中的应用范围要比数组广泛的多,它用于存放不知道长度的数据,且后续使用过程中可能会频繁的插入和删除元素。

初始化

切片的初始化方式有以下几种

var nums []int // 值
nums := []int{1, 2, 3} // 值
nums := make([]int, 0, 0) // 值
nums := new([]int) // 指针

可以看到切片与数组在外貌上的区别,仅仅只是少了一个初始化长度。通常情况下,推荐使用make来创建一个空切片,只是对于切片而言,make函数接收三个参数:类型,长度,容量。举个例子解释一下长度与容量的区别,假设有一桶水,水并不是满的,桶的高度就是桶的容量,代表着总共能装多少高度的水,而桶中水的高度就是代表着长度,水的高度一定小于等于桶的高度,否则水就溢出来了。所以,切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会,更多细节前往参考手册 - 长度与容量open in new window

使用

切片的基本使用与数组完全一致,区别只是切片可以动态变化长度,下面看几个例子。

切片可以通过append函数实现许多操作,函数签名如下,slice是要添加元素的目标切片,elems是待添加的元素,返回值是添加后的切片。

func append(slice []Type, elems ...Type) []Type

首先创建一个长度为0,容量为0的空切片,然后在尾部插入一些元素,最后输出长度和容量。

nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 可以看到长度与容量并不一致。

插入元素

切片元素的插入也是需要结合append函数来使用,现有切片如下,

nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

从头部插入元素

nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

从中间下标i插入元素

nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3,[1 2 3 4 999 999 5 6 7 8 9 10]

从尾部插入元素,就是append最原始的用法

nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

删除元素

切片元素的删除需要结合append函数来使用,现有如下切片

nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

从头部删除n个元素

nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]

从尾部删除n个元素

nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]

从中间指定下标i位置开始删除n个元素

nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]

删除所有元素

nums = nums[:0]
fmt.Println(nums) // []

字符串

普通字符串

普通字符串由""双引号表示,支持转义,不支持多行书写,下列是一些普通字符串

"这是一个普通字符串\n"
"abcdefghijlmn\nopqrst\t\uvwxyz"
这是一个普通字符串
abcdefghijlmn
opqrst  \uvwxyz

原生字符串

原生字符串由反引号表示,不支持转义,支持多行书写,原生字符串里面所有的字符都会原封不动的输出,包括换行和缩进。

`这是一个原生字符串,换行
	tab缩进,\t制表符但是无效,换行
	"这是一个普通字符串"
	
	结束
`
这是一个原生字符串,换行
        tab缩进,\t制表符但是无效,换行
        "这是一个普通字符串"

        结束

访问

因为字符串本质是字节数组,所以字符串的访问形式跟数组切片完全一致,例如访问字符串第一个元素

func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

输出是字节而不是字符

116

切割字符串

func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

尝试修改字符串元素

func main() {
   str := "this is a string"
   str[0] = 'a' // 无法通过编译
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

虽然没法修改字符串,但是可以覆盖

func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

函数

声明

函数的声明格式如下

func 函数名([参数列表]) [返回值] {
	函数体
}

声明函数有两种办法,一种是通过func关键字直接声明,另一种就是通过var关键字来声明,如下所示

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

var sum = func(a int, b int) int {
	return a + b
}

函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子,函数名称为Sum,有两个int类型的参数ab,返回值类型为int

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

还有一个非常重要的点,即Go中的函数不支持重载,像下面的代码就无法通过编译

type Person struct {
	Name    string
	Age     int
	Address string
	Salary  float64
}

func NewPerson(name string, age int, address string, salary float64) *Person {
	return &Person{Name: name, Age: age, Address: address, Salary: salary}
}

func NewPerson(name string) *Person {
	return &Person{Name: name}
}

Go的理念便是:如果签名不一样那就是两个完全不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。这种理念是否正确见仁见智,至少在Go中你可以仅通过函数名就知道它是干什么的,而不需要去找它到底是哪一个重载。

参数

Go中的参数名可以不带名称,一般这种是在接口或函数类型声明时才会用到,不过为了可读性一般还是建议尽量给参数加上名称

type ExWriter func(io.Writer) error 

type Writer interface {
	ExWrite([]byte) (int, error)
}

对于类型相同的参数而言,可以只需要声明一次类型,不过条件是它们必须相邻

func Log(format string, a1, a2 any) {
	...
}

变长参数可以接收0个或多个值,必须声明在参数列表的末尾,最典型的例子就是fmt.Printf函数。

func Printf(format string, a ...any) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

返回值

下面是一个简单的函数返回值的例子,Sum函数返回一个int类型的值。

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

当函数没有返回值时,不需要void,不带返回值即可。

func ErrPrintf(format string, a ...any) {
	_, _ = fmt.Fprintf(os.Stderr, format, a...)
}

Go允许函数有多个返回值,此时就需要用括号将返回值围起来。

func Div(a, b float64) (float64, error) {
	if a == 0 {
		return math.NaN(), errors.New("0不能作为被除数")
	}
	return a / b, nil
}

Go也支持具名返回值,不能与参数名重复,使用具名返回值时,return关键字可以不需要指定返回哪些值。

func Sum(a, b int) (ans int) {
	ans = a + b
	return
}

和参数一样,当有多个同类型的具名返回值时,可以省略掉重复的类型声明

func SumAndMul(a, b int) (c, d int) {
	c = a + b
	d = a * b
	return
}

不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。

func SumAndMul(a, b int) (c, d int) {
	c = a + b
	d = a * b
    // c,d将不会被返回
	return a + b, a * b
}

特性

  • 语法简单 Go语言在自由度和灵活度上做了取舍,以此换来了更好的维护性和平滑的学习曲线。
  • 部署友好 Go静态编译后的二进制文件不依赖额外的运行环境,编译速度也非常快。
  • 交叉编译 Go仅需要在编译时简单设置两个参数,就可以编译出能在其它平台上运行的程序
  • 天然并发 Go语言对于并发的支持是纯天然的,仅需一个关键字,就可以开启一个异步协程。
  • 垃圾回收 Go有着优秀的GC性能,大部分情况下GC延时都不会超过1毫秒。
  • 丰富的标准库 从字符串处理到源码AST解析,功能强大且丰富的标准库是Go语言坚实的基础。
  • 完善的工具链 Go有着完善的开发工具链,涵盖了编译,测试,依赖管理,性能分析等方方面面。

Go语言抛弃了继承,弱化了OOP,类,元编程,泛型,Lamda表达式等这些特性,拥有良好的性能和较低的上手难度,它适合用于云服务开发,应用服务端开发,以及网络编程。它自带GC,不需要开发者手动管理内存,静态编译和交叉编译这两点对于运维而言也十分友好。

以下简单介绍部分特性

并发与协程(Goroutine)

Go语言的并发模型基于协程(Goroutine)。协程是一种轻量级的线程,由Go运行时管理,可以在不同的线程中执行,调度是由Go语言的运行时进行的,对开发者透明。

go myFunction()

使用go关键字,可以非常简单地启动一个新的协程。

接口(Interface)

Go的接口类型是一种指定一组方法签名的类型,它是一种契约,不包含任何实现。一个类型只需要实现了接口声明的所有方法,就认为实现了该接口。

type Reader interface {
    Read(p []byte) (n int, err error)
}

错误处理

Go语言中的错误处理是通过返回值来实现的,通常函数会返回一个额外的错误值。调用者需要检查这个错误值来决定如何处理错误。

err := someFunction()
if err != nil {
    // 处理错误
}

映射(Map)

映射是Go语言中存储键值对的数据结构。它在内部是无序的,并且键的类型必须是支持比较操作的。

m := make(map[string]int)
m["key"] = 42

函数多返回值

Go语言的函数可以返回多个值,这使得函数可以同时返回成功值和错误信息,或者多个相关的值。

func div(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

总结

以上便是go的常用基础语法和特性介绍,如果想要了解更多go语言相关可以前往官网参考手册:The Go Programming Language Specificationopen in new window 进行深入学习