Go 语言学习笔记(一):基础知识

it2026-02-15  12

目录

语言简介

初识 Go 程序

Go 词法单元

变量和常量

复合数据类型


语言简介

已经有那么多种编程语言了,为什么还要发明新语言?为什么还要去学习新语言?相信不少人都有这样的疑问。答案很简单,虽然有那么多种语言,但每种语言都有其独特的应用领域,在某个领域使用某种语言能达到收益/投入的最大化。比如在嵌入式领域,汇编和 C 是首选;在操作系统领域,C 是首选;在系统级服务编程领域,C++ 是首选;在企业级应用程序和 Web 应用领域,Java 是首选。就好比木工的工具箱中锤子可以有很多种,大厨的工具箱中刀子有很多种一样,某种语言就像某种锤子或者某种刀一样,有其特别应用的领域 。

 

Go 语言的诞生主要基于如下原因 :

(1) 摩尔定律接近失效后多核服务器己经成为主流,当前的编程语言对并发的支持不是很好,不能很好地发挥多核 CPU 的威力。

(2) 程序规模越来越大,编译速度越来越慢,如何快速地编译程序是程序员的迫切需求。

(3) 现有的编程语言设计越来越复杂,由于历史的包袱,某些特性的实现不怎么优雅,程序员花费了更多的精力来应对编程语法细节而不是问题域。

Go 语言就是为了解决当下编程语言对 并发支持不友好编译速度慢编程复杂 这三个问题而诞生的 。

 

 

初识 Go 程序

package main import "fmt" func main() { /* 这是我的第一个简单的程序 */ fmt.Println("Hello, World!") }

Linux环境下编译 go 程序的方法:go build asd.go,然后使用 ./asd 运行相应的程序。或者直接使用 go run asd.go 运行程序。

让我们来看下以上程序的各个部分:

(1) 第 1 行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

(2) 第 3 行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包,fmt 包实现了格式化 IO 的函数。

(3) 第 5 行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

(4) 第 7 行 fmt.Println(...) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。使用 fmt.Print("hello, world\n") 可以得到相同的结果。Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量输出到控制台。

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected)。

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果你打算将多个语句写在同一行,它们必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

 

Go 代码的特征解读

(1) 源程序以 .go 为后缀。

(2) 源程序默认为 UTF-8 编码。

(3) 标识符区分大小写,大写字母开头的标识符可以被外部包的代码所使用。

(4) 语句结尾的分号可以省略。

(5) 函数 func 开头,函数体开头的 " { " 必须在函数头所在行尾部,不能单独起一行,否则会报错。

(6) 宇符串字面量使用 " "(双引号)括起来。

(7) 调用包里面的方法通过点 " . " 访问符,比如示例中的 fmt.Println

(8) main 函数所在的包名必须是 main。

 

 

Go 词法单元

在介绍 Go 语言具体语法之前,先介绍一下现代高级语言的源程序内部的几个概念: token、关键字、标识符、操作符、分隔符和字面量。

token

token 是构成源程序的基本不可再分割的单元。编译器编译源程序的第一步就是将源程序分割为一个个独立的 token,这个过程就是词法分析。Go 语言的 token 可以分为关键字、标识符、操作符、分隔符和字面常量等,分类如图所示。

Go 语言里面的 token 是怎么分割的?Go 的 token 分隔符有两类:一类是 操作符,还有一类自身没有特殊含义,仅用来分隔其他 token,被称为 纯分隔符。

操作符:操作符就是一个天然的分隔符,同时其自身也是一个 token,语句如下所示: sum := a+b

其中," := " 和 " + " 既是分隔符,也是 token,所以这个简单的语句被分割为 5 个 token:" sum "" := ""  a "、" + "" b ",Go 语言操作符的相关详细内容后面会涉及。

纯分隔符:其本身不具备任何语法含义,只作为其他 token 的分割功能。包括空格制表符换行符回车符,多个相邻的空格或者制表符会被编译器看作分隔符处理,例如:package main。

这是一个包声明的语句,package 和 main 之间可以有任意多个空格或者制表符,Go 编译器会将其作为一个分隔符处理,最后分离出来两个 token : package 和 main。

 

 

标识符

编程语言的标识符用来标识变量、类型、常量等语法对象的符号名称,其在语法分析时作为一个 token 存在。编程语言的标识符总体上分为两类:一类是语言设计者预留的标识符, 一类是编程者可以自定义的标识符。前者一般由语言设计者确定,包括语言的预声明标识符及用于后续语言扩展的保留字;后者是用户在编程过程中自行定义的变量名、常量名、函数名等一切符合语言规范的标识符。有一点需要注意,用户自定义的标识符不应该使用语言设计者的预留标识符,这可能导致歧义,并严重影响代码的可读性。

Go 的标识符构成规则是:开头一个字符必须是字母或下划线,后面任意多个字符、数字或下划线,并且区分大小写,Unicode 字符也可以作为标识符的构成,但是一般不推荐这么使用。我们在定义新的标识符时要避开 Go 语言预声明标识符,以免引起混乱。

Go 语言预声明的标识符包括关键字、内置数据类型标识符、常量值标识符、内置函数和空白标识符。在写 Go 源程序的过程中,用户自定义标识符用在包名、函数名、自定义类型名、变量名和常量名等上

 

 

关键字

编程语言里面的关键字是指语言设计者保留的有特定语法含义的标识符,这些关键字有自己独特的用途和语法含义,它们一般用来控制程序结构,每个关键字都代表不同语义的语法糖。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。举个例子:在 C 语言里用 a[i] 表示 *(a+i),由此可见语法糖不是 "现代语言" 独有,这种写法简洁明了,容易被人理解。

Go 语言是一门极简的语言,只有如下 25 个关键字 :

这 25 个关键字按照功能又可以分为三个部分:

引导程序整体结构的 8 个关键字

package    定义包名的关键字 import     导入包名关键字 const      常量声明关键字 var        变量声明关键字 func       函数定义关键字 defer      延迟执行关键字 go         并发语法糖关键字 return     函数返回关键字

 

声明复合数据结构的 4 个关键字

struct     定义结构类型关键字 interface  定义接口类型关键字 map        声明或创建map类型关键字 chan       声明或创建通道类型关键字

 

控制程序结构的 13 个关键字

if else          if else语句关键字 for range break continue    for 循环使用的关键字 switch select type case default fallthrough    switch 和 select 语句使用的关键字 goto     goto 跳转语句

 

内置数据类型标识符

丰富的内置类型支持是高级语言的基本特性,基本类型也是构造用户自定义类型的基础。为了标识每种内置数据类型,Go 定义了一套预声明标识符,这些标识符用在变量或常量声明时。Go 语言内置了 20 个预声明数据类型标识符。Go 语言按类别有以下几种数据类型:

布尔型:bool 整型:byte、int、int8、int16、int32、int64、uint、unint8、uint16、uint32、uint64、uintprt 浮点型:float32、float64 复数:complex64、complex128 错误类型:error 字符和字符串型:rune、string

Go 是一种强类型静态编译型语言,在定义变量和常量时需要显式地指出数据类型,当然 Go 也支持自动类型推导,在声明初始化内置类型变量时,Go 可以自动地进行类型推导。但是在定义新类型或函数时,必须显式地带上类型标识符。 

 

内置函数

make、new、len、cap、append、copy、delete、panic、recover、close、complex、real、image、print、println。内置函数也是高级语言的一种语法糖,由于其是语言内置的,不需要用 import 引入,内置函数具有全局可见性。注意到其中有以小写字母开头的,但是并不影响其全局可用性。

 

常量值标识符

true false   true 和 false 表示 bool 类型的两常量值:真和假 iota   用在连续的枚举类型的声明中 nil    指针/引用型的变量的默认值就是 nil 

 

空白标识符

_ ,空白标识符有特殊的含义,用来声明-个匿名的变量,该变量在赋值表达式的左端,空白标识符引用通常被用作占位,比如忽略函数多个返回值中的一个和强制编译器做类型检查。

 

Go 的源程序基本构成:

(1) 关键字引导程序的基本结构。

(2) 内置类型标识符辅助声明变量和常量。

(3) 字面量辅助变量和常量的初始化。

(4) 分隔符帮助 Go 编译器识别各个 token。

(5) 操作符、变量和关键字一起构成丰富的语法单元。

 

 

变量和常量

变量

变量使用一个名称来绑定一块内存地址,该内存地址中存放的数据类型由定义变量时指定的类型决定,该内存地址里面存放的内容可以改变。Go 语言变量声明:

(1) 显式的完整声明

var varName dataType = value 例如: var a int = 1 var a int = 2*3 var a int = b

关键字 var 用于变量声明,varName 是变量名标识符,dataType 是基本类型。value 是变量的初始值,初始值可以是字面量,也可以是其他变量名,还可以是一个表达式;如果不指定初始值,则 Go 默认将该变量初始化为类型的零值。Go 的变量声明后就会立即为其分配空间。

 

(2) 根据值自动判断变量类型

var v_name = value package main import "fmt" func main() { var d = true fmt.Println(d) }

 

(3) 短类型声明

varName := value package main import "fmt" func main() { f := "Runoob" // var f string = "Runoob" fmt.Println(f) }

注意:" := " 声明只能出现在函数内而不可以用于全局变量的声明与赋值:= 左侧如果没有声明新的变量,就产生编译错误。

var intVal int intVal :=1 // 这时候会产生编译错误 intVal,intVal1 := 1,2 // 此时不会产生编译错误,因为有声明新的变量,因为 := 是一个声明语句 package main import "fmt" func main() { f := "Runoob" // var f string = "Runoob" fmt.Println(f) }

 

多变量声明

类型相同多个变量, 非全局变量 var vname1, vname2, vname3 type vname1, vname2, vname3 = v1, v2, v3 var vname1, vname2, vname3 = v1, v2, v3 和 python 很像,不需要显示声明类型,自动推断 vname1, vname2, vname3 := v1, v2, v3 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误 这种因式分解关键字的写法一般用于声明全局变量 var ( vname1 v_type1 vname2 v_type2 ) 多变量可以在同一行进行赋值,如: var a, b, c int var c string a, b, c = 5, 7, "abc" package main var x, y int var ( // 这种因式分解关键字的写法一般用于声明全局变量 a int b bool ) var c, d int = 1, 2 var e, f = 123, "hello" //这种不带声明格式的只能在函数体中出现 //g, h := 123, "hello" func main(){ g, h := 123, "hello" println(x, y, a, b, c, d, e, f, g, h) }

如果函数体内的一个值声明了但是没有使用,会报错(declared and not used)。只有个使用了那个值后,错误才会移除。但是全局变量是允许声明但不使用的。例如:下面的代码只会报 a declared and not used 的错,而 testasd 不会影响程序。

package main import "fmt" var testasd int func main() { var a string = "abc" fmt.Println("hello, world") }

 

空白标识符在函数返回值时的使用:

package main import "fmt" func main() { _, numb, strs := numbers() //只获取函数返回值的后两个 fmt.Println(numb,strs) } //一个可以返回多个值的函数 func numbers()(int,int,string){ a , b , c := 1 , 2 , "str" return a,b,c }

 

 

常量

常量使用一个名称来绑定一块内存地址,该内存地址中存放的数据类型由定义常量时指定的类型决定,而且该内存地址里面存放的内容不可以改变。Go 中常量分为布尔型、宇符串型和数值型常量。常量存储在程序的只读段里(.rodata section)。

常量的定义格式:const identifier [type] = value,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

显式类型定义: const b string = "abc" 隐式类型定义: const b = "abc" package main import "fmt" func main() { const LENGTH int = 10 const WIDTH int = 5 var area int const a, b, c = 1, false, "str" //多重赋值 area = LENGTH * WIDTH fmt.Printf("面积为 : %d", area) println() println(a, b, c) }

 

常量作枚举

const ( Unknown = 0 Female = 1 Male = 2 )

常量可以用 len(),cap(),unsafe.Sizeof() 函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:

package main import "unsafe" const ( a = "abc" b = len(a) c = unsafe.Sizeof(a) ) func main(){ println(a, b, c) }

 

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量。iota 在 const 关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

package main import "fmt" func main() { const ( a = iota //0 b //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i //8 ) fmt.Println(a,b,c,d,e,f,g,h,i) } 输出:0 1 2 ha ha 100 100 7 8

 

 

变量作用域

作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。Go 语言中变量可以在三个地方声明:函数内定义的变量称为局部变量函数外定义的变量称为全局变量函数定义中的变量称为形式参数

package main import "fmt" /* 声明全局变量 */ var a int = 20; func main() { /* main 函数中声明局部变量 */ var a int = 10 var b int = 20 var c int = 0 fmt.Printf("main()函数中 a = %d\n", a); c = sum( a, b); fmt.Printf("main()函数中 c = %d\n", c); } /* 函数定义-两数相加 */ func sum(a, b int) int { fmt.Printf("sum() 函数中 a = %d\n", a); fmt.Printf("sum() 函数中 b = %d\n", b); return a + b; }

 

 

字符串

Go 语言将字符串作为一种原生的基本数据类型, 字符串的初始化可以使用字符串字面量。例如 :var a = "hello, world" 。

(1) 字符串是常量,可以通过类似数组的索引访问其字节单元,但是不能修改某个字节的值。例如 :

var a = "hello, world" b : = a [0] a[1] = 'a ' //error

(2) 字符串转换为切片 [ ]byte(s要慎用,尤其是当数据量较大时(每转换一次都需复制内容)。例如:

a := "hello, world!" b : = []byte (a)

(3) 字符串尾部不包含 NULL 字符,这一点和 C/C++ 不一样。

(4) 字符串类型底层实现是一个二元的数据结构,一个是指针指向字节数组的起点,另一个是长度。 例如 :

type stringStruct struct { str unsafe.Pointer   //指向底层字节数组的指针 len int              //字节数组长度 }

(5) 基于字符串创建的切片和原字符串指向相同的底层字符数组,一样不能修改,对字符串的切片操作返回的子串仍然是 string,而非 slice。例如:

a := "hello , world!" b := a[0:4] c := a[1: ] d = a[ :4]

(6) 字符串和切片的转换:字符串可以转换为字节数组,也可以转换为 Unicode 的字数组。例如:

a := "hello,世界!" b := [ ]byte(a) c := [ ]rune(a)

(7) 字符串的运算。例如:

a := "hello" b := "world" c := a + b   // 字符串的拼接 len(a)   // 内置的 len 函数获取字符串长度 d := "hello,世界!" for i := 0; i < len(d); i++ { fmt . Println(d[i]) } for i, v := range d {          fmt . Println(i , v) }

 

rune 类型

Go 内置两种字符类型:一种是 byte 的字节类类型( byte 是 uint 的别名,另一种是表示 Unicode 编码的字符 runerune 在 Go 内部是 int32 类型的别名,占用 4 个字节。Go 语言默认的字符编码就是 UTF-8 类型的,如果需要特殊的编码转换,则使用 Unicode/UTF-8 标准包。

package main import "fmt" func main() { asd := "123" var str []rune = []rune(asd) print(len(str)) fmt.Printf("%c" , str[0]) }

 

 

复合数据类型

顾名思义,复合数据类型就是由其他类型组合而成的类型。Go 语言基本的复合数据类型有指针、数组、切片、字典(map)、通道、结构和接口,它们的字面量格式如下:

* pointerType     指针类型使用*后面跟其指向的类型名 [n] elementType   数组类型使用[n]后面跟数纽元素类型来表示,n 表示该数组的长度 [] elementType    切片类型使用[]后面跟切片元素类型来表示 map [keyType]valueType   map 类型使用 map[键类型]值类型来表示 chan valueType    通道使用 chan 后面跟通道元素类型来表示 struct {            结构类型使用 struct{} 将各个结构字段扩起来表示 feildType feildType feildType feildType ... } interface {        接口类型使用 interface{}将各个方法括起来表示 method1(inputParams) (returnParams) method2(inputParams) (returnParams) ... }

 

指针

Go 语言支持指针,指针的声明类型为 *T,Go 同样支持多级指针 **T。通过在变量名前加 来获取变量的地址。指针的特点如下:

(1) 在赋值语句中,*T 出现在 "" 左边表示指针声明,*T 出现在 "" 右边表示取指针指向的值( varName 为变量名)。示例如下:

var a = 11 p := &a     *p 和 a 的位都是 11

(2) 结构体指针访问结构体字段仍然使用 "." 点操作符,Go 语言没有 "->" 操作符。例如:

type User struct{ name string age int } andes := User{ name:"andes", age:18, } p := &andes fmt.Println(p.name)     p.name 通过 "." 操作符访问成员变量

(3) Go 不支持指针的运算。

Go 由于支持垃圾回收,如果支持指针运算,则会给垃圾回收的实现带来很多不便,在 C 和 C++ 里面指针运算很容易出现问题,因此 Go 直接在语言层面禁止指针运算。例如

a := 1234 p := &a      Go语言里面自增、自减操作符是语句而不是表达式 p++         不允许,报 non-numeric type *int 错误

(4) 函数中允许返回局部变量的地址,Go 编译器使用"栈逃逸" 机制将这种局部变量的空间分配在堆上。例如:

func sum(a, b int) *int { sum := a + b return &sum        允许,sum会分配在 heap 上 } package main import "fmt" func main() { var a int= 20 /* 声明实际变量 */ var ip *int /* 声明指针变量 */ ip = &a /* 指针变量的存储地址 */ fmt.Printf("a 变量的地址是: %x\n", &a ) /* 指针变量的存储地址 */ fmt.Printf("ip 变量储存的指针地址: %x\n", ip ) /* 使用指针访问值 */ fmt.Printf("*ip 变量的值: %d\n", *ip ) } 空指针判断: if(ptr != nil) // ptr 不是空指针 if(ptr == nil) // ptr 是空指针 package main import "fmt" func main() { /* 定义局部变量 */ var a int = 100 var b int= 200 fmt.Printf("交换前 a 的值 : %d\n", a ) fmt.Printf("交换前 b 的值 : %d\n", b ) /* 调用函数用于交换值 * &a 指向 a 变量的地址 * &b 指向 b 变量的地址 */ swap(&a, &b); fmt.Printf("交换后 a 的值 : %d\n", a ) fmt.Printf("交换后 b 的值 : %d\n", b ) } func swap(x *int, y *int) { var temp int temp = *x /* 保存 x 地址的值 */ *x = *y /* 将 y 赋值给 x */ *y = temp /* 将 temp 赋值给 y */ } package main import "fmt" func main() { var a int var ptr *int var pptr **int a = 3000 /* 指针 ptr 地址 */ ptr = &a /* 指向指针 ptr 地址 */ pptr = &ptr /* 获取 pptr 的值 */ fmt.Printf("变量 a = %d\n", a ) fmt.Printf("指针变量 *ptr = %d\n", *ptr ) fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr) }

 

 

数组

数组的类型名是 [n]elemetType,其中 n 是数组长度,elementType 是数组元素类型。 比如一个包含 2 个 int 类型元素的数组类型可表示为 [2]int数组一般在创建时通过字面量初始化单独声明一个数组类型变量而不进行初始化是没有意义的。

数组初始化

a := [3]int{ 1, 2, 3}    指定长度和初始化字面量 a := [ ... ]int{ 1, 2, 3}  不指定长度,但是由后面的初始化列表数量来确定其长度 a := [3]int{ 1:1, 2:3}   指定总长度,并通过索引值进行初始化,没有初始化元素时使用类型默认值 a := [ ... ]int{ 1:1, 2:3}  不指定总长度,通过索引值进行初始化,数组长度由最后一个索引值确定,没有指定索引的元素被初始化为类型的零值

数组的特点

(1) 数组创建完长度就固定了,不可以再追加元素。

(2) 数组是值类型的,数组赋值或作为函数参数都是值拷贝。

(3) 数组长度是数组类型的组成部分,[10]int 和 [20]int 表示不同的类型。

(4) 可以根据数组创建切片。

package main import "fmt" func main() { var n [10]int /* n 是一个长度为 10 的数组 */ var i,j int /* 为数组 n 初始化元素 */ for i = 0; i < 10; i++ { n[i] = i + 100 /* 设置元素为 i + 100 */ } /* 输出每个数组元素的值 */ for j = 0; j < 10; j++ { fmt.Printf("Element[%d] = %d\n", j, n[j] ) } } package main import "fmt" func main() { /* 数组长度为 5 */ var balance = [5]int {1000, 2, 3, 17, 50} var avg float32 /* 数组作为参数传递给函数 */ avg = getAverage( balance, 5 ) ; /* 输出返回的平均值 */ fmt.Printf( "平均值为: %f ", avg ); } func getAverage(arr [5]int, size int) float32 { var i,sum int var avg float32 for i = 0; i < size;i++ { sum += arr[i] } avg = float32(sum) / float32(size) return avg; }

 

 

切片(Slice)

Go 语言切片是对数组的抽象。Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片"动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

type slice struct { array unsafe.Pointer len int cap int )

Go 为切片维护三个元素——向底层数组的指针切片的元素数量底层数组的容量。具体结构如图所示:

定义切片

声明一个未指定大小的数组来定义切片切片不需要说明长度

var identifier []type

或使用 make() 函数来创建切片:

var slice1 []type = make([]type, len) 也可以简写为 slice1 := make([]type, len) 也可以指定容量,其中capacity为可选参数。 make([]T, length, capacity) 这里 len 是数组的长度并且也是切片的初始长度。

 

切片初始化

s :=[] int {1,2,3 } 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3。其cap = len = 3 s := arr[:] 初始化切片s,是数组arr的引用 s := arr[startIndex:endIndex] 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片 s := arr[startIndex:] 默认 startIndex 时将表示从 arr 的第一个元素开始 s1 := s[startIndex:endIndex] 通过切片 s 初始化切片 s1 s := make([]int,len,cap) 通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片

 

len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。以下为具体实例:

package main import "fmt" func main() { var numbers = make([]int,3,5) printSlice(numbers) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

 

空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

package main import "fmt" func main() { var numbers []int printSlice(numbers) if(numbers == nil){ fmt.Printf("切片是空的") } } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

 

切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:

package main import "fmt" func main() { /* 创建切片 */ numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers) /* 打印原始切片 */ fmt.Println("numbers ==", numbers) /* 打印子切片从索引1(包含) 到索引4(不包含)*/ fmt.Println("numbers[1:4] ==", numbers[1:4]) /* 默认下限为 0*/ fmt.Println("numbers[:3] ==", numbers[:3]) /* 默认上限为 len(s)*/ fmt.Println("numbers[4:] ==", numbers[4:]) numbers1 := make([]int,0,5) printSlice(numbers1) /* 打印子切片从索引 0(包含) 到索引 2(不包含) */ number2 := numbers[:2] printSlice(number2) /* 打印子切片从索引 2(包含) 到索引 5(不包含) */ number3 := numbers[2:5] printSlice(number3) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

 

append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

package main import "fmt" func main() { var numbers []int printSlice(numbers) /* 允许追加空切片 */ numbers = append(numbers, 0) printSlice(numbers) /* 向切片添加一个元素 */ numbers = append(numbers, 1) printSlice(numbers) /* 同时添加多个元素 */ numbers = append(numbers, 2,3,4) printSlice(numbers) /* 创建切片 numbers1 是之前切片的两倍容量*/ numbers1 := make([]int, len(numbers), (cap(numbers))*2) /* 拷贝 numbers 的内容到 numbers1 */ copy(numbers1,numbers) printSlice(numbers1) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }

 

 

map

Go 语言内置的字典类型叫 mapmap 的类型格式是:map[K]T,其中 K 可以是任意可以进行比较的类型,T 是值类型。map 也是一种引用类型。

(1) map 的创建

使用字面量创建。例如:

/* 声明变量,默认 map 是 nil */ var map_variable map[key_data_type]value_data_type ma := map[string]int{ "a": 1, "b": 2) fmt.Println(ma["a"] ) fmt.Println(ma["b"])

使用内置的 make 函数创建。例如:

/* 使用 make 函数 */ map_variable := make(map[key_data_type]value_data_type) make(map[K]T ) //map 的容量使用默认位 make(map[K]T, len) //map 的容量使用给定的 len 值 mp1 := make(map[int]string) mp2 := make(map[int]string , 10) mp1[1] = "tom" mp2[1] = "pony" fmt.Println(mp1[1]) //tom fmt.Println(mp2[1]) //pony

 

(2) map 支持的操作

map 的单个键值访问格式为 mapName[key],更新某个 key 的值时 mapName[key放到等号左边,访问某个 key 的值时 mapName[key] 放在等号的右边。

可以使用 range 遍历一个 map 类型变量,但是不保证每次选代元素的顺序。

删除 map 中的某个键值,使用如下语法:delete(mapName,key)。delete 是内置函数,用来删除 map 中的某个键值对。

可以使用内置的 len() 函数返回 map 中的键值对数量。例如:

mp := make(map[int]string) mp[1] = "tom" mp[1] = "pony" mp[2] = "jaky" mp[3] = "andes " delete (mp , 3) fmt.Println (mp [1]) fmt.Println(len(mp))  // len函数返回 map 中的键值对的数量 for k, v := range mp {   // range 支持边历 mp,但不保证每次遍历次序是一样的 fmt.Println(" key=", k,"value=", v) }

注意:

(1) Go 内置的 map 不是并发安全的,并发安全的 map 可以使用标准包 sync 中的 map。

(2) 不要直接修改 map value 内某个元素的值,如果想修改 map 的某个键值,则必须整体赋值。例如:

type User struct { name string age int } ma := make(map[int]User) andes := User{ name : "andes", age: 18 , } ma[1] = andes // ma [1].age = 19 // ERROR,不能通过 map 引用直接修改 andes.age = 19 ma[1] = andes   // 必须整体替换 value fmt.Printf (" %v\n ", ma) package main import "fmt" func main() { var countryCapitalMap map[string]string /*创建集合 */ countryCapitalMap = make(map[string]string) /* map插入key - value对,各个国家对应的首都 */ countryCapitalMap [ "France" ] = "巴黎" countryCapitalMap [ "Italy" ] = "罗马" countryCapitalMap [ "Japan" ] = "东京" countryCapitalMap [ "India " ] = "新德里" /*使用键输出map值 */ for country := range countryCapitalMap { fmt.Println(country, "首都是", countryCapitalMap [country]) } /*查看元素在集合中是否存在 */ capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */ /*fmt.Println(capital) */ /*fmt.Println(ok) */ if (ok) { fmt.Println("American 的首都是", capital) } else { fmt.Println("American 的首都不存在") } } 输出: France 首都是 巴黎 Italy 首都是 罗马 Japan 首都是 东京 India 首都是 新德里 American 的首都不存在 package main import "fmt" func main() { /* 创建map */ countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"} fmt.Println("原始map") /* 打印map */ for country := range countryCapitalMap { fmt.Println(country, "首都是", countryCapitalMap [ country ]) } /*删除元素*/ delete(countryCapitalMap, "France") fmt.Println("法国条目被删除") fmt.Println("删除元素后map") /*打印map*/ for country := range countryCapitalMap { fmt.Println(country, "首都是", countryCapitalMap [ country ]) } } 输出: 原始map India 首都是 New delhi France 首都是 Paris Italy 首都是 Rome Japan 首都是 Tokyo 法国条目被删除 删除元素后map Italy 首都是 Rome Japan 首都是 Tokyo India 首都是 New delhi

 

 

范围Range

Go 语言中 range 关键字用于 for 循环中迭代数组、切片、通道channel或集合map的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

package main import "fmt" func main() { //这是我们使用range去求一个slice的和。使用数组跟这个很类似 nums := []int{2, 3, 4} sum := 0 for _, num := range nums { sum += num } fmt.Println("sum:", sum) //在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。 for i, num := range nums { if num == 3 { fmt.Println("index:", i) } } //range也可以用在map的键值对上。 kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) } //range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。 for i, c := range "go" { fmt.Println(i, c) } } 输出: sum: 9 index: 1 a -> apple b -> banana 0 103 1 111

 

 

struct

Go 中的 struct 类型和 C 类似,由多个不同类型元素组合而成。这里面有两层含义:第一 ,struct 结构中的类型可以是任意类型第二, struct 的存储空间是连续的,其段按照声明时的顺序存放(注意段之间有对齐要求)。struct 有两种形式:一种是 struct 类型面量,另一种是使用 type 声明的自定义 struct 类型。

(1) struct 类型字面量的声明格式:

struct { FeildName FeildType FeildName FeildType FeildName FeildType }

(2) 自定义 struct 类型声明格式:

type TypeName struct { FeildName FeildType FeildName FeildType FeildName FeildType }

实际使用 struct 字面量的场景不多,更多的时候是通过 type 自定义一个新的类型来实现的。type 是自定义类型的关键字,不但支持 struct 类型的创建,还支持任意其他子定义类型的创建。

 

(3) struct 类型变量的初始化。示例如下:

type Person struct { Name string Age int } type Student struct { *Person Number int } a := Person{"Tom", 21) 按照类型声明顺序,逐个赋值,一旦 struct 增加字段,则 整个初始化语句会报错 推荐下面这种使用 Feild 名字的初始化方式,没有指定的字段则默认初始化为类型的零值 p := &Person{ Name:"tata", Age: 12 , } s := Student{ Person: p, Number: 110, } package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* Declare Book1 of type Book */ var Book2 Books /* Declare Book2 of type Book */ /* book 1 描述 */ Book1.title = "Go 语言" Book1.author = "www.runoob.com" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 /* book 2 描述 */ Book2.title = "Python 教程" Book2.author = "www.runoob.com" Book2.subject = "Python 语言教程" Book2.book_id = 6495700 /* 打印 Book1 信息 */ printBook(&Book1) /* 打印 Book2 信息 */ printBook(&Book2) } func printBook( book *Books ) { fmt.Printf( "Book title : %s\n", book.title) fmt.Printf( "Book author : %s\n", book.author) fmt.Printf( "Book subject : %s\n", book.subject) fmt.Printf( "Book book_id : %d\n", book.book_id) }

 

 

控制结构

现代计算机存储结构无论 "普林斯顿结构",还是 "哈佛结构",程序指令都是线性地存放在存储器上。程序执行从本质上来说就是两种模式:顺序和跳转。

顺序就是按照程序指令在存储器上的存放顺序逐条执行。 跳转就是遇到跳转指令就跳转到某处继续线性执行。

Go 是一门高级语言,其源程序虽然经过了高度的抽象并封装了很多语法糖,但还是跳不出这个模式(这里暂时不考虑 goroutine 引入并发后的执行视图变化)。

顺序在 Go 里面体现在从 main 函数开始逐条向下执行,就像我们的程序源代码顺序一样;跳转在 Go 里面体现为多个语法糖,包括 goto 语句和函数调用、分支( if、switch 、select )、循环( for )等。跳转分为两种:一种是无条件跳转,比如函数调用和 goto 语句;一种是有条件的跳转,比如分支和循环。

备注:

Go 的源代码的顺序并不一定是编译后最终可执行程序的指令顺序,这里面涉及语言的运行时和包的加载过程。上面论述的主要目的是使读者从宏观上整体理解程序的执行过程,建立一个从源代码到执行体的大体映射概念,这个概念不那么精准,但对我们理解源程序到目标程序的构建非常有帮助。

 

if 语句特点

(1) if 后面的条件判断子句不需要用小括号括起来。

(2) " { " 必须放在行尾,和 if 或 if else 放在一行。

(3) if 后面可以带一个简单的初始化语句,并以分号分割,该简单语句声明的变量的作用域是整个 if 语句块,包括后面的 else if 和 else 分支。

(4) Go 语言没有条件运算符 (a > b ? a:b),这也符合 Go 的设计哲学,只提供一种方法事情。

(5) if 分支语句遇到 return 后直接返回,遇到 break 则跳过 break 下方的 if 语句块。

if x <= y { return y } else { return x } if x :=f(); x<y {  // 初始化语句中的声明变量 x return x } else if x > z {  // x 在 else if 里面一样可以被访问 return z } else { return y } package main import "fmt" func main() { /* 局部变量定义 */ var a int = 100; /* 判断布尔表达式 */ if a < 20 { /* 如果条件为 true 则执行以下语句 */ fmt.Printf("a 小于 20\n" ); } else { /* 如果条件为 false 则执行以下语句 */ fmt.Printf("a 不小于 20\n" ); } fmt.Printf("a 的值为 : %d\n", a); }

 

switch 语

switch 语句会根据传入的参数检测并执行符合条件的分支 。switch 的语法特点如下:

(1) switch 和 if 语句一样,switch 后面可以带一个可边的简单的初始化语句。

(2) switch 后面的表达式也是可选的,如果没有表达式,则 case 子句是一个布尔表达式,而不是一个值,此时就相当于多重 if else 句。

(3) switch 条件表达式的值不像 C 语言那样必须限制为整数,可以是任意支持相等比较运算的类型变量。

(4) 通过 fallthough 语句来强制执行下一个 case 子句(不再判断下一个 case 子句的条件是否满足)。

(5) switch 支持 default 语句,当所有的 case 分支都不符合时,执行 default 句, 并且 default 语句可以放到任意位置,并不影响 switch 的判断逻辑。

(6) switch 和 .(type) 结合可以进行类型的查询。

switch i := " Y "; i{     //switch 后面可以带上一个初始化语句 case "y"," Y":      //多个 case 值使用逗号分隔 fmt.Println ("yes" ) //yes fallthrough          //fallthrough 会跳过接下来的 case 条件表达式 //直接执行下一个 case 语句 case "n","N": fmt . Println(" no" )   // no } switch { case score >= 90: grade = 'A' case score >= 80 : grade ='B' case score >= 70 : grade ='C' case score >= 60 : grade = 'D' default : grade ='F' } fmt.Printf(" grade = %c\n ", grade)    //grade=B package main import "fmt" func main() { /* 定义局部变量 */ var grade string = "B" var marks int = 90 switch marks { case 90: grade = "A" case 80: grade = "B" case 50,60,70 : grade = "C" default: grade = "D" } switch { case grade == "A" : fmt.Printf("优秀!\n" ) case grade == "B", grade == "C" : fmt.Printf("良好\n" ) case grade == "D" : fmt.Printf("及格\n" ) case grade == "F": fmt.Printf("不及格\n" ) default: fmt.Printf("差\n" ); } fmt.Printf("你的等级是 %s\n", grade ); } package main import "fmt" func main() { switch { case false: fmt.Println("1、case 条件语句为 false") fallthrough case true: fmt.Println("2、case 条件语句为 true") fallthrough case false: fmt.Println("3、case 条件语句为 false") fallthrough case true: fmt.Println("4、case 条件语句为 true") case false: fmt.Println("5、case 条件语句为 false") fallthrough default: fmt.Println("6、默认 case") } } 输出: 2、case 条件语句为 true 3、case 条件语句为 false 4、case 条件语句为 true

 

for 语

Go 语言仅支持一种循环语句,即 for 语句,同样遵循 Go 的设计哲学,只提供一种方法做事情,把事情做好。Go 对应 C 循环的三种场景如下:

(1) 类似 C 里面的 for 循环语句

for init; condition; post { } init:一般为赋值表达式,给控制变量赋初值; condition:关系表达式或逻辑表达式,循环控制条件; post:一般为赋值表达式,给控制变量增量或减量。 package main import "fmt" func main() { sum := 0 for i := 0; i <= 10; i++ { sum += i } fmt.Println(sum) }

(2) 类似 C 里面的 while 循环语句

for condition { } package main import "fmt" func main() { sum := 1 for ; sum <= 10; { sum += sum } fmt.Println(sum) // 这样写也可以,更像 While 语句形式 for sum <= 10{ sum += sum } fmt.Println(sum) }

(3) 类似 C 里面的 while (1) 死循环语句

for { } package main import "fmt" func main() { sum := 0 for { sum++ // 无限循环下去 } fmt.Println(sum) // 无法输出 }

for 还有一种用法,是对数组、切片、符串、map 和通道的访问,语法格式如下:

访问 map for key, value := range map{} for key := range map{} 访问数组 for index, value := range arry{} for index := range arry{} for , value := range arry{} 访问切片 for index, value := range slice{} for index := range slice{} for _, value := range slice{} 访问通道 for value := range channel {} package main import "fmt" func main() { strings := []string{"google", "runoob"} for i, s := range strings { fmt.Println(i, s) } numbers := [6]int{1, 2, 3, 5} for i,x:= range numbers { fmt.Printf("第 %d 位 x 的值 = %d\n", i,x) } } 输出: 0 google 1 runoob 第 0 位 x 的值 = 1 第 1 位 x 的值 = 2 第 2 位 x 的值 = 3 第 3 位 x 的值 = 5 第 4 位 x 的值 = 0 第 5 位 x 的值 = 0

 

 

标签和跳转

Go 语言使用标签 ( Lable )来标识一个语句的位置,用于 goto、break、continue 语句的跳转,标签的语法是:Lable:Statement。

goto

goto 语句用于函数的内部的跳转,需要配合标签一起使用,具体的格式如下: goto Lable

goto Lable 的语义是跳转到标签名后的语句处执行,goto 语句有以下几个特点:

(1) goto 语句只能在函数内跳转。

(2) goto 语句不能跳过内部变量声明语句,这些变量在 goto 语句的标签语句处又是可见的。例如:

goto L  //BAD,跳过 v := 3 这条语句是不允许的 v := 3 L:

(3) goto 语句只能跳到同级作用域或者上层作用域内,不能跳到内部作用域内。例如:

if n%2 == 1 { goto L1 } for n > 0 { f() n-- L1: f() n-- } package main import "fmt" func main() { //print9x() gotoTag() } //嵌套for循环打印九九乘法表 func print9x() { for m := 1; m < 10; m++ { for n := 1; n <= m; n++ { fmt.Printf("%dx%d=%d ",n,m,m*n) } fmt.Println("") } } //for循环配合goto打印九九乘法表 func gotoTag() { for m := 1; m < 10; m++ { n := 1 LOOP: if n <= m { fmt.Printf("%dx%d=%d ",n,m,m*n) n++ goto LOOP } else { fmt.Println("") } n++ } }

 

break

break 用于函数内跳出 for、switch、select 语句的执行,有两种使用格式:

(1) 单独使用,用于跳出 break 当前所在的 for、switch、select 语句的执行。

(2) 和标签一起使用,用于跳出标签所标识的 for、switch 、select 语句的执行。可用于跳出多重循环,但标签和 break 必须在同一个函数内。例如:

L1: for i := 0; ; i++{ for j := 0; ; j ++ { if i >= 5 { // 跳出 L1 标签所在的 for 循环 break L1 } if j > 10 { // 默认仅跳出离 break 最近的内层循环 break } } } package main import "fmt" func main() { // 不使用标记 fmt.Println("---- break ----") for i := 1; i <= 3; i++ { fmt.Printf("i: %d\n", i) for i2 := 11; i2 <= 13; i2++ { fmt.Printf("i2: %d\n", i2) break } } // 使用标记 fmt.Println("---- break label ----") re: for i := 1; i <= 3; i++ { fmt.Printf("i: %d\n", i) for i2 := 11; i2 <= 13; i2++ { fmt.Printf("i2: %d\n", i2) break re } } } 输出: ---- break ---- i: 1 i2: 11 i: 2 i2: 11 i: 3 i2: 11 ---- break label ---- i: 1 i2: 11

 

continue

continue 用于跳出 for 循环的本次选代,跳到 for 循环的下一次选代的 post 语句处执行,也有两种使用格式:

(1) 单独使用,用于跳出 continue 当前所在的 for 循环的本次迭代。

(2) 和标签一起使用,用于跳出标签所标识的 for 语句的本次选代,但标签和 continue 必须在同一个函数内。例如:

L1: for i := 0; ; i++ { for j := 0 ; ; j ++ { if i >= 5 { // 跳到 Ll 标签所在的 for 循环 i++ 处执行 continue L1 // the following is not executed } if j > 10 { continue } } } package main import "fmt" func main() { // 不使用标记 fmt.Println("---- continue ---- ") for i := 1; i <= 3; i++ { fmt.Printf("i: %d\n", i) for i2 := 11; i2 <= 13; i2++ { fmt.Printf("i2: %d\n", i2) continue } } // 使用标记 fmt.Println("---- continue label ----") re: for i := 1; i <= 3; i++ { fmt.Printf("i: %d\n", i) for i2 := 11; i2 <= 13; i2++ { fmt.Printf("i2: %d\n", i2) continue re } } } 输出: ---- continue ---- i: 1 i2: 11 i2: 12 i2: 13 i: 2 i2: 11 i2: 12 i2: 13 i: 3 i2: 11 i2: 12 i2: 13 ---- continue label ---- i: 1 i2: 11 i: 2 i2: 11 i: 3 i2: 11

 

最新回复(0)