Go语言语法笔记

Posted Naisu Xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言语法笔记相关的知识,希望对你有一定的参考价值。

文章目录

前言

这篇文章主要用于记录Go语言相关语法,方便自己查阅使用。

Go语言基础使用相关内容可以参考下面文章:
https://blog.csdn.net/Naisu_kun/article/details/126757496

这篇文章主要参考自下面内容:
A Tour of Go
The Go Programming Language Specification

本文中Go版本为 1.19

基础语法

数据类型

基础的数据类型主要有 布尔(Boolean)字符串(String)数值(Numeric) 等一些(这几个最简单,就放这里一起说了)。

布尔类型:
这个类型只有 bool 一种,取值是 truefalse

字符串类型:
这个类型指 string ,该类型可以使用 len() 函数获取长度,可以使用 [] 来取其中的字节(只能读不能写,事实上Go中的字符串其实是只读byte数组的一个切片):

通常使用 "" 包围的内容是字符串,使用 '' 包为的是字符,其内容特殊的字符需要 \\ 进行转义。Go语言中也可以使用 `` (键盘上Esc下面的按键)包围表示字符串,其内部内容是不会进行转义的,也可以用它来表示多行字符串。

数值类型:
数值类型主要有下面一些:
int int8 int16 int32 int64 有符号类型
uint uint8 uint16 uint32 uint64 uintptr 无符号类型
byte rune
float32 float64 单精度浮点型与双精度浮点型
complex64 complex128 复数类型

byteuint8 的别名。
runeint32 的别名,通常用来表示一个Unicode编码值。

int uintuintptr 长度与程序位宽相同,其中 uintptr 主要用于保存指针。

零值(Zero values,默认值):
布尔类型的零值(Zero values)是 false
字符串的零值是 "" ,即空字符串。
数值类型的零值是 0

对于已声明但未赋值的变量,Go语言会赋零值给它。

类型转换:
Go语言中类型间不会自动隐式转换,需要手动进行类型转换。使用 T(v) 方式,T 表示要转换成的类型。

变量与常量

编程就是对数据的操作,变量 (Variables)常量(Constants) 等操作是最基础的部分。

使用 var 关键词可以声明变量,下面是变量基本的声明与赋值操作:

var name string       // 声明一个 string类型 的变量 name
var flagA, flagB bool // 声明 布尔类型 的变量 flagA和flagB

var i int = 233                // 声明一个 int 的变量 i ,并赋值为 233
var e, pi float32 = 2.71, 3.14 // 声明 float32类型 的变量 e和pi ,并分别赋值为 2.71和3.14

// 对于已声明但未赋值的变量,Go语言会赋零值给它

// 声明一组变量
var (
	flagC bool
	j     int
	str   string = "Naisu"
)

// 如果声明变量的时候赋值的话也可以省略变量类型,这种清空下程序会自动推导其类型
var v1 = true         // 布尔型系统推导为 bool
var v2 = "Naisu"      // 字符串类型推导为 string
var v3 = 47           // 整数自动推导为 int
var v4 = 3.14         // 小数自动推导为 float64
var v5 = 0.618 + 0.5i //复数自动推导为 complex128

var m, n = "Naisu", 233 // 省略类型自动推导的话可以一行声明多个类型的变量

函数内部可以使用 := 代替 var 声明变量,这称为短变量声明(Short variable declarations):

func main() 
	var i int            // 普通声明
	j := 3               // 短变量声明,短变量声明必须赋初值
	m, n := "Naisu", 233 // 声明多个变量

可以使用 const 关键词来声明常量,常量在声明时必须赋值,并且之后无法再修改其值。

算术与赋值运算符

对于变量的操作最常见的就是算术运算和赋值运算,两个过程中主要涉及的就是相关的操作符。

算术运算符(Arithmetic operators):

运算符操作运算符操作
+-
*/
%
&按位与|按位或
^按位异或(按位取反)&^按位清零
<<左移>>右移

赋值运算符(Assignment operators):
赋值运算符最核心的就是 = ,其功能就是将右边的值赋给左边的变量。

剩下的赋值运算符都是该功能和算术运算符的复合,比如 += ,表达式 i += 1 就是指 i = i + 1 。其它的赋值运算符还有 -= *= /= %= &= |= ^= &^= <<= >>=

流程控制语句

if 语句:
if语句基本的使用方法和其它语言差不多,可以结合else等使用:

x := 99
if x < 80 
	...
 else if (x >= 80) && (x < 90) 
	...
 else 
	...

Go中的if可以在条件表达式前声明变量,该变量仅在该if及其else等内部有效:

if x := 99; x < 80 
	...

switch 语句:
Go中switch相比if与其它语言的变化稍微大一点,下面是个示例:

switch os := runtime.GOOS; os 
case "windows":
	fmt.Println("Windows.")
case "linux":
	fmt.Println("Linux.")
default:
	fmt.Printf("%s.\\n", os)

switch和if一样可以(当然也可以没有)在条件表达式前声明变量,该变量在switch语句块中有效。

每个 case 语句块后面默认有一个 break ,你也可以使用 fallthrough 来取消这里的 break ,比如下面示例:

Go中switch也可以省略条件表达式,这可以当作一种简化的 if-else 方式:

i := 2
switch 
case i == 2:
	...
case i == 1:
	...
default:
	...

for 语句:
for语句是目前Go中唯一的循环语句。

和其它语言中的循环语句一样可以使用 break 来跳出循环,使用 continue 来跳过本次循环。

for语句基本的使用方法和其它语言差不多:

for i := 0; i < 8; i++ 
	...

for语句可以省略初始化与后置语句,相当于其它语言中的 while 语句来使用:

k := 1
for k < 16 
	k += k

Go中的for也可以用作无限循环:

for  ...  // 等同于 for true  ... 

Go中的for可以和range组合起来遍历字符串、数组、切片等内容:

goto 语句:
goto就和其它语言中发goto一样,用来跳转到某处:

goto L // 直接跳转到标签L处
...
L:
...

比较与逻辑运算符

流程控制过程中常用到比较与逻辑运算符,运算最终获得一个布尔值。

比较运算符(Comparison operators):
== 相等 、 != 不相等;
< 小于 、 <= 小于等于;
> 大于 、 >= 大于等于。

逻辑运算符(Logical operators):
&& 逻辑与, || 逻辑或, ! 逻辑非。

函数

Go中函数定义方式如下:

func 函数名(输入参数) (返回值类型) 
   // 函数体内容

Go中函数和其它语言比较大的区别主要有两点:

  • 返回值类型是放在输入参数后面函数体之前的;
  • 可以有多个返回值;

下面是一些具体的函数示例:

// 没有输入参数与返回值的函数
func fn1() 
// 只有单个输入参数,没有返回值的函数
func fn2(str string)  fmt.Println(str) 
// 有两个同类型输入参数,没有返回值的函数
func fn3(x, y int)  fmt.Println(x + y) 
// 有两个不同类型输入参数,没有返回值的函数
func fn4(name string, data int)  fmt.Println(name, data) 
// 没有输入参数,有单个返回值的函数
func fn5() string  return "Hello Naisu!" 
// 没有输入参数,有两个返回值的函数
func fn6() (string, int)  return "Hello", 233 
// 有输入参数和返回值的函数
func fn7(a, b int) (int, int)  return b, a 

Go函数的返回值有一种命名返回值(Named return values)的写法:

func plus(a int, b int) (sum int) 
	sum = a + b
	return // 相当于return sum


func split(sum int) (x, y int) 
	x = sum * 4 / 9
	y = sum - x
	return // 相当于return x, y

Go的函数如果有多个返回值,需要用到所有返回值或者个别返回值的话,需要按次序一一接受返回值,对于不需要的返回值可以用 _ 表示:

package main

func fn6() (string, int)  return "Hello", 233 

func main() 
	str1, d1 := fn6() // 依次接收两个返回值
	str2, _ := fn6()  // 只接收第一个返回值
	_, d2 := fn6()    // 只接收第二个返回值
	fn6()             // 两个返回值都不要

Go的函数可以支持传入不定个数的参数(使用 ...):

函数也可以使用下面方式定义:

var fn1 = func() 
var fn3 = func(x, y int)  fmt.Println(x + y) 
var fn6 = func() (string, int)  return "Hello", 233 
var fn7 = func(a, b int) (int, int)  return b, a 
// 这种方式也有被叫做匿名函数

从上面方式可以中其实可以理解为函数也是一个变量,函数名即是变量名(当然只是可以这么理解而已,实际上不是这么回事,事实上使用指针的概念更加贴切些)。

既然函数何以看作是变量,那就也可以作为输入参数使用,这就是回调函数了:

函数类型有时候写起来会很长,可以使用 type 关键词来简化编写:

// 将 func(int, int) (int, int) 类型声明为 fntype 
type fntype func(int, int) (int, int)

func mainfn(f fntype)  ... 

除了输入参数可以用函数,返回类型也可以是函数,这个经常拿来做闭包使用:

defer
Go中可以使用defer修饰语句,使其推迟到函数返回后再执行,这在处理某些用完后需要释放的资源时比较有用。

defer有三个特性:

defer修饰的语句定义时内部的值就会立刻确定,但语句会等到函数返回时才执行:

一个函数中有多个defer时会压入栈中,函数返回时会按照 后进先出 的顺序调用:

defer语句可以操作命名返回值:

更多数据类型

指针(pointer)

Go和C/C++一样也有指针。 类型 *T 是指向 T 类型值的指针。其零值为 nil Go中的指针和C/C++的指针挺像的,但是没有指针运算功能:

结构体(struct)

Go中的结构体和C/C++的结构体基本的功能挺相似的:

结构体变量有多种创建方式:

数组(array)

数组本身功能和使用比较简单:

短变量声明数组的时候也可以使用 arr := [...]int22, 33 这种方式,编译器会自动计算数组长度。

你可以使用 [x][y]T 来声明二维数组,更高维的同理。

需要注意的是当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。为了避免复制数组,你可以传递一个指向数组的指针。

切片(slice)

Go的数组是定长的,有些时候并不是很方便,所以Go还提供了一个切片功能。实际开放中切片反而是更加常用的功能。

类型 []T 表示一个元素类型为 T 的切片。切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:s[low : high] ,它会选择一个半开区间,包括第一个元素,但排除最后一个元素。切片下界的默认值为 0 ,上界则是该切片的长度。

切片只是对其底层数组的引用,修改切片中内容的时候其实修改的是其底层的数据,反过来底层数据变动也会引起切片数据变动:

切片也可以不从现有数组直接创建,这种情况下其实底层会创建一个数组,然后对其进行切片引用:

arr := [6]int2, 4, 6, 8, 10 // 数组创建时是包含初始长度的
s := []int2, 4, 6, 8, 10 // 切片创建时不需要长度

切片拥有 长度(len)容量(cap) 。切片的长度就是它所包含的元素个数。切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。可以通过对切片重新切片来调整长度和容量。

可以使用 make 函数来创建切片,这也会在底层自动创建数组:

s1 := make([]int, 5) // 指定类型和长度
s2 := make([]int, 3, 7) // 指定类型、长度和容量

可以使用 func append(s []T, vs ...T) []T 函数动态的向切片追加数据,当底层数组容量不够时会自动创建新数组:

映射(map)

Go中的映射可以存储键值对集合,形式为 map[KeyType]ElementType ,Go中的映射键和值的类型都几乎可是任意类型。映射容量不够时再插入键值对是会自动扩容的。


需要注意的是映射是引用类型,传统过程中传递的只是引用:

可以使用for和range来遍历映射,但需要注意的是映射是无序的。

泛型(generic)

为 generics 可以让你用同一块代码来处理多种不同的数据类型,它为避免重复,方便替换复杂数据结构等提供了方便。


上面例子中 [T int | string | float64] 内容就是泛型了,这里将 int\\string\\float64 三个类型定义为 T,后面使用时 T 就可以是这三个之一了。

如果要用到的类型很多,可以使用下面方式来定义(其实就是接口啦):

type myT interface 
	int | int8 | int16 | int32 | int64

这样使用只需要用 [T myT] 这样的方式即可。事实上Go已经内置了两个这种接口: any 表示go里面所有的内置基本类型; comparable 表示go里面所有内置的可比较类型。

方法与接口

方法

Go里面是没有类的,但事实上面向对象的设计思想还是比较好用的,在C语言中使用结构体一定程度上可以代替部分面向对象的功能,对象的方法虽然可以使用函数指针来处理,但是并不优雅。Go里面在那个基础上稍微改进了方法的处理方式。

Go里面可以在函数名前面加上一个接收者(接收者类型必须是当前包中的自定义类型,通常使用自定义的结构体类型),这样函数就变成了该接收者类型方法了。下面是个简单的示例:

方法接收者处理上还有一些更多的处理方式:

func (t *T) name()  // 最常用的方式,如果要修改接收者的话就得用这个
func (t T) name()  // 如果只需要读不需要修改接收者的话也可以使用这个方式
func (T) name()  // 如果不需要读写接收者的话可以使用这个方式

接口

接口的具体细节的内容比较多,这里稍微记录一下。

接口最常用的功能就是定义一组方法,其它的结构体等对象可以实现一个接口中的部分或全部、或者多个接口中的方法。

Go本身内置了很多接口。比如下面这个:

type Stringer interface 
    String() string // 这在打印输出时很有用,以字符串形式来表达自己

接口是可以嵌套的:

type animal interface 
	peo
	dog
	cat


type peo interface 
type dog interface 
type cat interface 

除了声明方法外 空接口 也是经常用的东西。空接口可以保存任何类型内容,在没有泛型之前空接口就经常拿来代替泛型使用。

可以使用类型断言来判断接口保存了什么数据:

类型断言也可以和switch等结合使用。

并发

Goroutine

goroutine是一个轻量级的线程或者说是协程,用来提供并发编程功能。使用 go 关键词修饰语句来启动goroutine:

Channel

启用多线程后不可避免的引入不同线程操作同一数据或是数据通讯的问题。在Go中最主要使用channel来处理相关问题。

var ch chan int // 声明channel
ch = make(chan int) // 使用前需要创建(长度为1)
// ch = make(chan int, 3) // 创建并指定长度

ch := make(chan string) // 直接创建channel(长度为1)
ch := make(chan string, 5) // 直接创建并指定长度

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

channel相当于一个先进先出的队列,一旦创建后长度就不会动态变化。写满时,无法继续写入;为空时,无法从取到数据。向channel发送将持续阻塞直到数据被接收;接收将持续阻塞直到收到数据。

可以使用 close(channel) 来关闭channel,关闭后就无法再向里面写入数据。可以使用 v, ok := <-ch 来接收数据,如果没有数据可用且channel已被关闭,则ok的值将为false。

循环 for i := range c 会不断从信道接收值,直到它被关闭。

select 语句:
select有点像switch,只不过是用在channel操作上。select会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

select 
    case <-ch1:
        // 如果从 ch1 信道成功接收数据,则执行该分支代码
    case ch2 <- 1:
        // 如果成功向 ch2 信道成功发送数据,则执行该分支代码
    default:
        // 如果上面都没有成功,则进入 default 分支处理流程
        // 可以没有default语句,这样它会一直阻塞直到某个case可以执行

sync.Mutex

上面的Channel是用于数据通讯的,但有时候会有多个线程操作同一个变量的情况,这个就有可能有问题产生,比如两个线程同时操作一个变量。通常解决方案就是sync.Mutex。

package main

import (
	"sync"
)

var (
	mu sync.Mutex // 惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的
	v  int        // 被保护的变量
)

func task() 
	mu.Lock()         // 上锁,上锁后其它线程将无法获取被保护的对变量
	defer mu.Unlock() // 用完后记得解锁
	v = 233


func main() 
	go task()

后记

这里只是粗略记录了一些Go的语法,更精细内容以后再补充。Go的语法本身感觉一般,很多功能总有一种一开始设计时完全没有考虑,后期打了个补丁加上去的感觉。不过Go的工具链用起来感觉倒是挺不错的。

以上是关于Go语言语法笔记的主要内容,如果未能解决你的问题,请参考以下文章

Go语言 语法详解笔记(上)

Go语言文件操作

Go语言语法笔记

Go语言语法笔记

go语言结构体及方法的一些细节笔记

Go语言学习笔记-1