runoob教程:Go 语言教程 | 菜鸟教程 (runoob.com)

环境搭建

安装包官方下载地址:Downloads - The Go Programming Language (golang.org)

国内能打开的地址:Downloads - The Go Programming Language (google.cn)

dAttEC.jpg

Windows安装go

安装包下载

这里我选择下载msi文件的go1.16.2.windows-amd64.zip压缩包,然后放在环境目录D:\Env\go1.16.2\bin下解压。

环境配置

将go目录下的/bin路径添加到Path环境变量中,点击新建。

D:\Env\go1.16.2\bin

检测安装

打开cmd输入go。

7xISvd.jpg

hello-world

在工作目录创建hello.go文件。

package main

import "fmt"

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

使用go run命令运行:

go run hello.go

go build生成二进制文件运行:

> go build .\hello.go
> ls
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         2021/3/15     10:50        2094592 hello.exe
-a----         2021/3/15     10:47             79 hello.go
> .\hello.exe
Hello, World!

Linux安装go

环境:Ubuntu 20.04.1(推荐使用WSL,但现在电脑上没配置,就在虚拟机上做了)

官方安装文档:Download and install - The Go Programming Language (google.cn)

安装包下载

下载go1.16.2.linux-amd64.tar.gz压缩文件。

解压

$ sudo tar -zxvf go1.16.2.linux-amd64.tar.gz

设置环境变量

$ export PATH=$PATH:/usr/local/go/bin

检测安装

$ go version

问题处理

本来想通过VMware Tools把压缩文件直接从主机拖到虚拟机的,但尝试本地解压VMware Tools失败,报错显示内存不足,尝试解压到其它目录。解决方法如下:

$ sudo tar -xzvf VmwareTools.tar.gz -C /home/oopsdc		# -C 后指定解压目录,我这里直接解压到用户目录下
$ cd /home/oopsdc/vmware-tools-distrib/					# 切换到对应目录
$ sudo ./vmware-install.pl								# 运行安装文件即可

Go语言结构

Go语言基础组成有以下几部分:

  1. 包声明
  2. 引入包
  3. 函数
  4. 变量
  5. 语句&表达式
  6. 注释

我们以之前hello.go的代码为例。

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}
  1. 第一行package main定义了包名。我们必须在源文件非注释的第一行指明这个文件属于哪个包。如package main表示一个可独立执行的程序,每个Go应用程序都包含一个main包。
  2. 第三行import fmt告诉Go编译器这个程序需要使用fmt包的函数或其他元素。fmt包中有实现格式化I/O的函数。
  3. 第五行的func main()为程序开始执行的函数。main函数是每个可执行程序必须包含的,除init()函数外一般都是第一个执行的函数。
  4. fmt.Println()可以将字符串输出到控制台并自动换行,效果与fmt.Println("Hello,World\n")相同。其中,Println和Print函数支持使用变量,如fmt.Println(arr),若不加特别指令,会以默认打印格式将变量arr输出到控制台。
  5. 当标识符(如变量、常量、类型、函数名及结构字段等)以一个大写字母开头,如:Variable1,那么使用这种形式的标识符对象就可以被外部包代码所使用(需要先导入这个包),这被称为导出(像面向对象语言中的public);若标识符以小写字母开头,则对包外不可见,但在包内部可见并可用(类似protected)。
  6. 前括号{不能单独一行,否则会报错
func main()
{ // 错误
    fmt.Println("Hello, World!")
}

学习笔记

变量声明三种方式

方式一

指定变量类型,若未初始化则值为0。

var v_name v_type
v_name = value

实例1:

package main
import "fmt"
func main() {
    // 声明一个变量并初始化
    var a = "oopsdc"
    fmt.Println(a)

    // 没有初始化就为零值
    var b int
    fmt.Println(b)

    // bool 零值为 false
    var c bool
    fmt.Println(c)
}

执行结果:

oopsdc
0
false
  • 数值类型(包括complex64/128)为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)
  • 以下几种类型为 nil:
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口

实例2:

package main

import "fmt"

func main() {
    var i int
    var f float64
    var b bool
    var s string
    fmt.Printf("%v %v %v %q\n", i, f, b, s)
}

执行结果:

0 0 false ""

方式二

根据值自行判断变量类型。

var v_name = value

实例:

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

执行结果:

true

方式三

:=赋值,是使用变量的首选形式。**但只能被用在函数体内,不能够用于全局变量声明及赋值。**使用:=操作符可以高效创建一个变量。称之为初始化声明。

v_name := value

注::=左侧需为未声明过的新变量,否则会报错。

例:

var intVal int 

intVal :=1 // 这时候会产生编译错误

intVal,intVal1 := 1,2 // 此时不会产生编译错误,因为有声明新的变量,因为 := 是一个声明语句

实例:

package main
import "fmt"
func main() {
    f := "oopsdc" // var f string = "oopsdc"

    fmt.Println(f)
}

执行结果:

oopsdc

多变量声明

//类型相同多个变量, 非全局变量
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
)

实例:

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)
}

执行结果:

0 0 0 false 1 2 123 hello 123 hello

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

实例:

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
}

执行结果:

2 str

unsafe.sizeof()函数

实例如下:

package main

import "unsafe"
const (
    a = "abc"
    b = len(a)
    c = unsafe.Sizeof(a)
)

func main(){
    println(a, b, c)
}

运行结果:

abc 3 16

看到的时候没搞懂为什么c,即unsafe.Sizeof(a)的值为16,于是查了一下。

csdn文章:unsafe.sizeof()_记录每一个小阶段的学习心得,持之以恒!-CSDN博客

为什么字符串类型的unsafe.Sizeof()一直为16

实际中,字符串类型对应一个结构体,结构体有两个域。第一个域为指向该字符串的指针,第二个域是字符串长度,每个域占8字节,但并不包含指针指向的字符串内容,也就是说它的返回值一直为16。

iota特殊常量

iota介绍

是一个特殊常量,可认为是可被编译器修改的常量。

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

iota可被用作枚举值,第一个iota值为0,每当其在新的一行被引用时,值自动加1:

const (
    a = iota	// 1
    b = iota	// 2
    c = iota	// 3
)

简写形式如下:

const (
    a = iota
    b
    c
)

iota用法

实例1:

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)
}

运行结果:

> go run .\iota1.go
0 1 2 ha ha 100 100 7 8

实例2:

package main

import "fmt"
const (
    i=1<<iota
    j=3<<iota
    k
    l
)

func main() {
    fmt.Println("i =",i)
    fmt.Println("j =",j)
    fmt.Println("k =",k)
    fmt.Println("l =",l)
}

运行结果:

> go run .\iota2.go
i = 1
j = 6
k = 12
l = 24

刚看到i=1<<iota的时候就想到了移位,以下是对结果的分析:

<<n==*(2^n)

i = 1 << 0		# 左移0位
i = 001
i = 1 * 2^0 = 1

j = 3 << 1		# 左移1位
j = 011 => 110
j = 3 * 2^1 = 6

k = 3 << 2		# 左移2位
k = 011 => 1100
k = 3 * 2^2 = 12

l = 3 << 3		# 左移3位
l = 011 => 11000
l = 3 * 2^3 = 24

常量

常量是一个简单值的标识符,在程序运行过程中不会改变。

常量中的数据类型只能为布尔型、数字型(整数型、浮点型和复数)及字符串型。

常见常量定义格式:

const identifier [type] = value

类型说明符[type]可省略,因为编译器可根据变量值推断其类型。

  • 显式类型定义
const a string = "oopsdc"
  • 隐式类型定义
const a = "oopsdc"
  • 多个常量定义
const a_name1, a_name2 = value1, value2

实例:

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)  
}

执行结果:

面积为 : 50
1 false str

常量用作枚举:

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

语言运算符

运算符用于在程序执行时执行数学或逻辑运算。

Go语言内置运算符有:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 其它运算符

其它运算符

运算符描述实例
&返回变量存储地址&a; 将给出变量的实际地址。
*指针变量。*a; 是一个指针变量

实例:

package main

import "fmt"

func main() {
   var a int = 4
   var b int32
   var c float32
   var ptr *int

   /* 运算符实例 */
   fmt.Printf("第 1 行 - a 变量类型为 = %T\n", a );
   fmt.Printf("第 2 行 - b 变量类型为 = %T\n", b );
   fmt.Printf("第 3 行 - c 变量类型为 = %T\n", c );

   /*  & 和 * 运算符实例 */
   ptr = &a     /* 'ptr' 包含了 'a' 变量的地址 */
   fmt.Printf("a 的值为  %d\n", a);
   fmt.Printf("*ptr 为 %d\n", *ptr);
}

执行结果:

第 1 行 - a 变量类型为 = int
第 2 行 - b 变量类型为 = int32
第 3 行 - c 变量类型为 = float32
a 的值为  4
*ptr 为 4

运算符优先级

优先级运算符
5* / % « » & &^
4+ - | ^
3== != < <= > >=
2&&
1||

我们可以通过使用括号来临时提升某个表达式的整体运算优先级。

条件判断语句

语句描述
if 语句if 语句 由一个布尔表达式后紧跟一个或多个语句组成。
if…else 语句if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。
if 嵌套语句你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。
switch 语句switch 语句用于基于不同条件执行不同动作。
select 语句select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

注:Go 没有三目运算符,不支持?:形式的条件判断。

循环语句

Go 语言提供了以下几种类型循环处理语句:

循环类型描述
for 循环重复执行语句块
循环嵌套在 for 循环中嵌套一个或多个 for 循环

循环控制语句

循环控制语句可以控制循环体内语句的执行过程。

GO 语言支持以下几种循环控制语句:

控制语句描述
break 语句经常用于中断当前 for 循环或跳出 switch 语句
continue 语句跳过当前循环的剩余语句,然后继续进行下一轮循环。
goto 语句将控制转移到被标记的语句。

无限循环

实例:

package main

import "fmt"

func main() {
    for true  {
        fmt.Printf("这是无限循环。\n");
    }
}

函数

函数定义

格式如下:

func function_name( [parameter list] ) [return_types] {
	函数体
}

函数定义解析:

  • func:函数由 func 开始声明
  • function_name:函数名称,函数名和参数列表一起构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。

实例:

// 返回两个数的较大者
func main(num1, num2 int) int {
    // 声明局部变量
	var result int
	
	if (num1 > num2) {
		result = num1
	} else {
		result = num2
	}
	return result
}

函数调用

实例:

package main

import "fmt"

func main() {
    // 定义局部变量
    var a int = 1
    var b int = 2
    var ret int
    
    // 调用函数返回最大值
    ret = max(a, b)
    
    fmt.Printf("Maximum: %d\n", ret)
}

// max()函数
func max(num1, num2 int) int {
    // 定义局部变量
    var res int
    
    if (num1 > num2) {
        res = num1
    } else {
        res = num2
    }
    return res
}

函数返回多个值

Go 函数可以返回多个值。

实例:

package main

import "fmt"

func main() {
    a, b := swap("dc", "oops")
    fmt.Println(a, b)
}

func swap(x, y string) (string, string) {
    return y, x
}

执行结果:

oops dc

函数参数

函数如果使用参数,该变量可称为函数的形参。

形参就像定义在函数体内的局部变量。

调用函数,可以通过两种方式来传递参数:

传递类型描述
值传递值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

函数用法

函数用法描述
函数作为另外一个函数的实参函数定义后可作为另外一个函数的实参数传入
闭包闭包是匿名函数,可在动态编程中使用
方法方法就是一个包含了接受者的函数

变量作用域

作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。

Go 语言中变量可以在三个地方声明:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

局部变量

在函数体内声明的变量称为局部变量,其作用域只在函数体内,参数和返回值也是局部变量。

实例:

package main

import "fmt"

func main() {
    // 声明局部变量
    var a, b, c int
    
    // 初始化参数
    a = 1
    b = 2
    c = a + b
    
    fmt.Printf("Res: a=%d, b=%d, c=%d\n", a, b, c)
}

执行结果:

Res: a=1, b=2, c=3

全局变量

在函数体外声明的变量称为全局变量,其作用域可以在整个包甚至外部包(被导出后)使用。

全局变量可在任何函数中使用。

实例1:

package main

import "fmt"

// 声明全局变量
var g int

func main() {
    // 声明局部变量
    var a, b int
    
    // 初始化参数
    a = 1
    b = 2
    g = a + b
    
    fmt.Printf("Res: a=%d, b=%d, g=%d\n", a, b, g)
}

执行结果:

Res: a=1, b=2, g=3

Go 语言中全局变量和局部变量名称可以相同,但位于函数内的局部变量会被优先考虑。

实例2:

package main

import "fmt"

// 声明全局变量
var g int = 10

func main() {
    // 声明局部变量
    var a, b, g int
    
    // 初始化参数
    a = 1
    b = 2
    g = a + b
    
    fmt.Printf("Res: a=%d, b=%d, g=%d\n", a, b, g)
}

执行结果:

Res: a=1, b=2, g=3

我们可以看出,即使在函数外部给全局变量g赋值为10,最后输出的还是函数体内局部变量g的值。

形式参数

形式参数会作为函数的局部变量来使用。

实例:

package main

import "fmt"

// 声明全局变量
var a int = 10

func main() {
    // main()函数中声明局部变量
    var a int = 1
    var b int = 2
    var c int = 0
    
    fmt.Printf("main()函数中 a=%d\n", a)
    c = sum(a, b)
    fmt.Printf("main()函数中 c=%d\n", c)
}

// sum()函数定义
func sum(a, b int) int {
    fmt.Printf("sum()函数中 a=%d\n", a)
    fmt.Printf("sum()函数中 b=%d\n", b)
    
    return a + b
}

执行结果:

main()函数中 a=1
sum()函数中 a=1
sum()函数中 b=2
main()函数中 c=3

初始化局部和全局变量

不同类型的局部和全局变量默认值为:

数据类型初始化默认值
int0
float320
pointernil

数组

Go 中提供了数组类型的数据结构。

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

相对于去声明 number0, number1, …, number99 的变量,使用数组形式 numbers[0], numbers[1] …, numbers[99] 更加方便且易于扩展。

数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为0,第二个索引为1,以此类推。

声明数组

语法格式如下:

var variable_name [SIZE] variable_type
// 数组名 数组长度 数组类型

如我们定义一个类型为int,长度为10的数组balance:

var balance [10] int

初始化数组

var balance = [5]float32{1,2,3,4,5}

还可以通过字面量在声明数组的同时快速初始化:

var balance := [5]float32{1,2,3,4,5}

当数组长度不确定时,我们可以使用...代替数组长度,编译器会根据数组元素个数自动推断长度:

var balance := [...]float32{1,2,3,4,5}

当设置了数组长度时,我们可以通过指定下标来初始化元素:

// 将索引为1和3的元素初始化
balance := [5]balance{1:1.0,3:3.0}

注:{}中的元素个数不能大于[]中的数字。

访问数组元素

实例1:

package main

import "fmt"

func main() {

	var arr [10] int		// 长度为10的数组
	var i, j int

	// 给数组初始化元素
	for i = 0; i < 10; i++ {
		arr[i] = i + 1;
	}

	// 输出每个元素的值
	for j = 0; j < 10; j++ {
		fmt.Printf("Ele[%d] = %d\n", j, arr[j])
	}
}

执行结果:

Ele[0] = 1
Ele[1] = 2
Ele[2] = 3
Ele[3] = 4
Ele[4] = 5
Ele[5] = 6
Ele[6] = 7
Ele[7] = 8
Ele[8] = 9
Ele[9] = 10

实例2:

package main

import "fmt"

func main() {

	var i, j int

	arr := [...]float32{1,2,3,4,5}

	for i = 0; i < 5; i++ {
		fmt.Printf("Ele[%d] = %f\n", i, arr[i])
	}

	arr2 := [5]float32{1:2.0,3:7.0}
	for j = 0; j < 5; j++ {
		fmt.Printf("Ele[%d] = %f\n", j, arr2[j])
	}

}

执行结果:

Ele[0] = 1.000000
Ele[1] = 2.000000
Ele[2] = 3.000000
Ele[3] = 4.000000
Ele[4] = 5.000000
Ele[0] = 0.000000
Ele[1] = 2.000000
Ele[2] = 0.000000
Ele[3] = 7.000000
Ele[4] = 0.000000

更多内容

数组对Go 语言来说非常重要,以下我们是关于数组更多的内容:

内容描述
多维数组Go 语言支持多维数组,最简单的多维数组是二维数组
向函数传递数组你可以向函数传递数组参数

指针

指针变量通常缩写为ptr。

指针使用

实例:

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)

}

执行结果:

a 变量地址为: c000128058
ip 变量存储地址为: c000128058
*ip 指向的值: 20

空指针

当指针没有被分配到任何变量时,值为nil。

实例:

package main

import "fmt"

func main() {
	var ptr *int
    fmt.Printf("%x\n", ptr)
}

执行结果:

0

空指针判断:

if (ptr != nil)
if (ptr == nil)

更多内容

内容描述
Go 指针数组你可以定义一个指针数组来存储地址
Go 指向指针的指针Go 支持指向指针的指针
Go 向函数传递指针参数通过引用或地址传参,在函数调用时可以改变其值

结构体

数组可以存储同一类型的数据,结构体中可以为不同项定义不同数据类型。

结构体是由一系列具有相同类型或不同类型数据构成的数据集合。

结构体表示一项纪录,比如一个学生有以下属性:

  • 姓名
  • 学号
  • 学院
  • 专业

定义结构体

使用type和struct语句。

type语句设定了结构体名称。

struct语句定义了一个新的数据类型,结构体中有一个或多个成员。

结构体格式如下:

type struct_variable_type struct {
    member definition
    member definition
    ...
}

定义了结构体类型,就能用于变量声明:

variable_name := structure_variable_type {value1, value2...}
variable_name := structure_variable_type {key1:value1, key2:value2...}

实例:

package main

import "fmt"

func main() {
    fmt.Println(Books{"Go lang", "DC", "Go lang learn", "oopsdc.tk"})
    fmt.Println(Books{title:"Go lang", author:"DC", subject:"Go lang learn", website:"oopsdc.tk"})
    fmt.Println(Books{title:"Go lang", author:"DC"})
}

type Books struct {
    title string
    author string
    subject string
    website string
}

执行结果:

{Go lang DC Go lang learn oopsdc.tk}
{Go lang DC Go lang learn oopsdc.tk}
{Go lang DC  }

访问结构体成员

格式:

结构体.成员名

实例:

package main

import "fmt"

func main() {
    var Book1 Books
    var Book2 Books

    Book1.title = "C语言"
    Book1.author = "DC"
    Book1.subject = "编程"
    Book1.id = 00001

    Book2.title = "Python语言"
    Book2.author = "DC"
    Book2.subject = "编程"
    Book2.id = 00002

    fmt.Printf("Book1 title: %s\n", Book1.title)
    fmt.Printf("Book1 author: %s\n", Book1.author)
    fmt.Printf("Book1 subject: %s\n", Book1.subject)
    fmt.Printf("Book1 id: %d\n", Book1.id)

    fmt.Printf("Book2 title: %s\n", Book2.title)
    fmt.Printf("Book2 author: %s\n", Book2.author)
    fmt.Printf("Book2 subject: %s\n", Book2.subject)
    fmt.Printf("Book2 id: %d\n", Book2.id)
}

type Books struct {
    title string
    author string
    subject string
    id int
}

执行结果:

Book1 title: C语言
Book1 author: DC
Book1 subject: 编程
Book1 id: 1
Book2 title: Python语言
Book2 author: DC
Book2 subject: 编程
Book2 id: 2

结构体作为函数参数

我们可以像其它数据类型一样将结构体类型作为参数传递给函数。

实例:

package main

import "fmt"

func main() {
    var Book1 Books
    var Book2 Books
    
    Book1.title = "C语言"
    Book1.author = "DC"
    Book1.subject = "编程"
    Book1.date = "2021-03-19"
    
    Book2.title = "Python"
    Book2.author = "DC"
    Book2.subject = "编程"
    Book2.date = "2021-03-19"
    
    printBook(Book1)
    printBook(Book2)
}

type Books struct {
    title string
    author string
    subject string
    date string
}

func printBook(book Books) {
    fmt.Printf("Title: %s\n", book.title)
    fmt.Printf("Author: %s\n", book.author)
    fmt.Printf("Subject: %s\n", book.subject)
    fmt.Printf("Date: %s\n", book.date)
}

执行结果:

Title: C语言
Author: DC
Subject: 编程
Date: 2021-03-19
Title: Python
Author: DC
Subject: 编程
Date: 2021-03-19

结构体指针

定义结构体指针其实类似于其它指针变量:

var strcut_pointer *ptr

以上定义的指针变量可以存储结构体变量地址。我们可以通过&符号查看结构体变量地址:

struct_pointer = &ptr

使用结构体指针访问结构体成员,使用.:

struct_pointer.title

我们用指针重写一下之前结构体的实例:

package main

import "fmt"

func main() {
    var Book1 Books
    var Book2 Books
    
     Book1.title = "C语言"
    Book1.author = "DC"
    Book1.subject = "编程"
    Book1.date = "2021-03-19"
    
    Book2.title = "Python"
    Book2.author = "DC"
    Book2.subject = "编程"
    Book2.date = "2021-03-19"
    
    printBook(&Book1)
    printBook(&Book2)
}

type Books struct {
    title string
    author string
    subject string
    date string
}

func printBook(book *Books) {
    fmt.Printf("Title: %s\n", book.title)
    fmt.Printf("Author: %s\n", book.author)
    fmt.Printf("Subject: %s\n", book.subject)
    fmt.Printf("Date: %s\n", book.date)
}

切片

切片是对数组的抽象。

Go 中数组长度不可变,不适用部分特定场景,Go 提供了一种灵活且功能强悍的内置类型——切片(“动态数组”)。与数组相比,切片长度不固定,可以追加元素,在追加时可能使切片容量增大。

定义切片

声明一个未指定长度的数组来定义切片:

var identifier []type

切片不需要指定长度。

我们还可以使用make()函数创建切片:

var slice1 []type = make([]type, len)
slice1 := make([]type, len)

也可以指定容量,capacity为可选参数:

make([]T, length, capacity)

此处的len是数组长度也是切片初始长度。

切片初始化

直接初始化切片,[]表示切片类型,{1,2,3}初始化值依次为1,2,3,cap=len=3:

s := [] int {1,2,3}

初始化切片s,是数组arr的引用:

s := arr[:]

将arr中下标为startIndex到endIndex-1下的元素创建为一个新的切片:

s := arr[startIndex:endIndex]

默认endIndex时将表示一直到arr的最后一个元素:

s := arr[startIndex:]

默认startIndex时表示从arr的第一个元素开始:

s := arr[:endIndex]

通过切片s初始化切片s1:

s1 := s[startIndex:endIndex]

通过内置函数make()初始化切片s,[]int标识其元素类型为int的切片:

s := make([]int, len, cap)

len()和cap()函数

切片可索引,且可由len()方法获取长度。

切片提供了计算容量的方法,cap()可以测量切片长度上限。

实例:

package main

import "fmt"

func main() {
    var nums = make([]int, 3, 5)
    
    printSlice(nums)
}

func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v", len(x), cap(x), x)
}

执行结果:

len=3 cap=5 slice=[0 0 0]

空切片

切片在未初始化之前默认为nil,长度为0。

实例:

package main

import "fmt"

func main() {
    var nums[] int
    
    printSlice(nums)
    
    if (nums == nil) {
        fmt.Println("切片为空!")
    }
}

func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

执行结果:

len=0 cap=0 slice=[]
切片为空!

切片截取

可通过设置上下限设置截取切片。

实例:

package main

import "fmt"

func main() {
    nums := []int{1,2,3,4,5,6,7,8,9}
    printSlice(nums)
    
    fmt.Println("nums=",nums)
    
    fmt.Println("nums[1:4]=",nums[1:4])
    
    fmt.Println("nums[:3]=",nums[:3])
    
    fmt.Println("nums[4:]=",nums[4:])
    
    nums1 := make([]int, 0, 5)
    printSlice(nums1)
    
    num2 := nums[:2]
    printSlice(num2)
    
    num3 := nums[2:5]
    printSlice(num3)
}

func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

执行结果:

len=9 cap=9 slice=[1 2 3 4 5 6 7 8 9]
nums= [1 2 3 4 5 6 7 8 9]
nums[1:4]= [2 3 4]
nums[:3]= [1 2 3]
nums[4:]= [5 6 7 8 9]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[1 2]
len=3 cap=7 slice=[3 4 5]

前闭后开。

append()与copy()函数

若想增加切片容量,我们要创建一个新的更大的切片并把原切片内容拷贝过来,接下来我们通过copy与append方法。

实例:

package main

import "fmt"

func main() {
    var nums []int
    printSlice(nums)
    
    nums = append(nums, 0)
    printSlice(nums)
    
    nums = append(nums, 1)
    printSlice(nums)
    
    nums = append(nums, 1, 2, 3)
    printSlice(nums)
    
    nums1 := make([]int, len(nums), (cap(nums))*2)
    
    copy(nums1, nums)
    printSlice(nums1)
}

func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

执行结果:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 1 2 3]
len=5 cap=12 slice=[0 1 1 2 3]

Range

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

实例:

package main

import "fmt"

func main() {
        nums := []int{2, 3, 4}
        sum := 0

        for _, num := range nums {
                sum += num
        }
        fmt.Println("sum:", sum)

        for i, num := range nums {
                if num == 3 {
                        fmt.Println("index:", i)
                }
        }

        kvs := map[string]string{"a":"apple", "b":"banana"}
        for k, v := range kvs {
                fmt.Printf("%s -> %s\n", k, v)
        }

        for i, c := range "go" {
                fmt.Println(i, c)
        }
}

执行结果:

sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

Map

Map是一种无需的键值对集合,它最重要的一点是通过key来快速检索数据,key类似于索引,指向数据的值。

我们可以像迭代数组和切片那样迭代Map,但Map时无序的,我们无法决定它的返回顺序,因为Map使用hash表实现。

定义Map

1.使用内建函数make

2.使用map关键字:

var map_variable map[key_data_type]value_data_type
map_variable := make(map[key_data_type]value_type)

若不初始化map,那么会创建一个nil map,nil map不能用来存放键值对。

实例:

package main

import "fmt"

func main() {
        var testMap map[string]string
        testMap = make(map[string]string)

        testMap ["人文"] = "法学"
        testMap ["数计"] = "计算机"
        testMap ["外国语"] = "翻译"
        testMap ["智能制造"] = "嵌入式"

        for aca := range testMap {
                fmt.Println(aca, "招牌是", testMap [aca])
        }

        maj, ok := testMap ["土木"]

        if (ok) {
                fmt.Println("土木的招牌是:", maj)
        } else {
                fmt.Println("值不存在")
        }
}

执行结果:

智能制造 招牌是 嵌入式
人文 招牌是 法学
数计 招牌是 计算机
外国语 招牌是 翻译
值不存在

delete()函数

delete()函数用于删除集合元素,参数为map和其对应的key。

实例:

package main

import "fmt"

func main() {
        testMap := map[string]string{"一": "1", "二": "2", "三": "3"}

        fmt.Println("原始数据")

        for i := range testMap {
                fmt.Println(i, "值为", testMap [i])
        }

        delete(testMap, "一")
        fmt.Println("一被删除")

        fmt.Println("删除后数据")

        for i := range testMap {
                fmt.Println(i, "值为", testMap [i])
        }
}

执行结果:

原始数据
一 值为 1
二 值为 2
三 值为 3
一被删除
删除后数据
三 值为 3
二 值为 2

递归函数

Go 支持递归,但在使用时需要设置退出条件,否则将陷入无限循环。

阶乘

import "fmt"

func main() {
        var i int = 15
        fmt.Printf("%d的阶乘为:%d\n", i, Factorial(uint64(i)))
}

func Factorial(n uint64)(res uint64) {
        if (n > 0) {
                res = n * Factorial(n-1)
                return res
        }
        return 1
}

执行结果:

15的阶乘为:1307674368000

斐波那契数列

package main

import "fmt"

func main() {
        var i int
        for i = 0; i < 10; i++ {
                fmt.Printf("%d\t", fibonacci(i))
        }
}

func fibonacci(n int) int {
        if n < 2 {
                return n
        }
        return fibonacci(n-2) + fibonacci(n-1)
}

执行结果:

0       1       1       2       3       5       8       13      21      34

类型转换

注:Go 不支持隐式转换

基本格式:

type_name(expression)
类型      表达式

实例:

package main

import "fmt"

func main() {
        var res float32
        var sum = 17
        var count = 5

        res = float32(sum)/float32(count)
        fmt.Printf("%f\n", res)
}

执行结果:

3.400000

接口

Go 中提供了另一种数据类型,接口。它把所有具有共性的方法定义在一起,任何其它类型只要实现了这些方法就是实现了这个接口。

实例:

package main

import "fmt"

func main() {
        var phone Phone

        phone = new(NokiaPhone)
        phone.call()

        phone = new (IPhone)
        phone.call()
}

type Phone interface {
        call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
        fmt.Println("This is Nokiad")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
        fmt.Println("iPhone plz!")
}

执行结果:

This is Nokiad
iPhone plz!

错误处理

Go 通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,定义如下:

type error interface {
	Error() string
}

我们可以在编码中通过实现error接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息,使用error New可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
}

我们在调用Sqrt的时候传递一个负数,然后得到non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,输出错误,实例如下:

res, err := Sqrt(-1)

if err != nil {
    fmt.Println(err)
}

实例:

package main

import "fmt"

func main() {
        if res, errorMsg := Divide(100, 10); errorMsg == "" {
                fmt.Println("100/10 = ", res)
        }

        if _, errorMsg := Divide(100, 0); errorMsg != "" {
                fmt.Println("errorMsg:", errorMsg)
        }
}

type DivideError struct {
        dividee int
        divider int
}

func (de *DivideError) Error() string {
        strFormat := `
        Cannot proceed, the divider is zero!
        dividee: %d
        divider: 0
`
        return fmt.Sprintf(strFormat, de.dividee)
}

func Divide(varDividee int, varDivider int) (res int, errorMsg string) {
        if varDivider == 0 {
                dData := DivideError {
                        dividee: varDividee,
                        divider: varDivider,
                }
                errorMsg = dData.Error()
                return
        } else {
                return varDividee / varDivider, ""
        }
}

执行结果:

100/10 =  10
errorMsg:
        Cannot proceed, the divider is zero!
        dividee: 100
        divider: 0

并发

Go 支持并发,我们只需通过go关键字开启goroutine即可。

goroutine是轻量级线程,调度是由Golang 运行时进行管理的。

goroutine语法格式:

go 函数名(参数列表)

如:

go f(x, y, z)

开启一个新routine:

f(x, y, z)

Go 允许使用go语句开启一个新的运行期线程,即goroutine。以一个不同的、新创建的goroutine来执行一个函数。同一个程序中所有goroutine共享一个地址空间。

实例:

package main

import (
        "fmt"
        "time"
)

func main() {
        go say("world")
        say("hello")
}

func say(s string) {
        for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
        }
}

执行结果:

world
hello
world
hello
hello
world
world
hello
hello

通道(channel)

通道是用来传递数据的一个数据结构,可用于两个goroutine之间通过传递一个指定类型的值来同步运行和通讯。操作符<-用于指定通道方向,发送或接收。若未指定方向,则为双向通道。

ch <- v			// 把v发送到通道ch
v := <-ch		// 从ch接收数据并把值赋给v

通道在使用前必须先创建:

ch := make(chan int)

注:默认情况下通道不带缓冲区。发送端发送数据,同时必须有接收端接收相应数据。

实例:

package main

import "fmt"

func main() {
        s := []int{7, 2, 8, -9, 4, 1}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c

        fmt.Println(x, y, x+y)
}

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum
}

执行结果:

-4 17 13

通道缓冲区

通道可以设置缓冲区,通过make的第二个参数指定缓冲区大小。

ch := make(chan int, 100)

带缓冲区的通道允许发送端数据发送和接收端的数据获取处于异步状态,但由于缓冲区大小有限,所以必须由接收端来接收数据。

注:若通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。若通道带缓冲,发送方会阻塞直到发送的值被拷贝到缓冲区中,若缓冲区已满,则意味着需要等待直到接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

实例:

package main

import "fmt"

func main() {
        ch := make(chan int, 2)

        ch <- 1
        ch <- 2

        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

执行结果:

1
2

Go遍历与关闭通道

Go通过range关键字来实现遍历读取到的数据,类似于数组或切片。格式如下:

v, ok := <- ch

若通道接收不到数据后ok为false,这时通道可以使用close()函数关闭。

实例:

package main

import "fmt"

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)

        for i := range c {
                fmt.Println(i)
        }
}

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
        }
        close(c)
}

执行结果:

0
1
1
2
3
5
8
13
21
34

文章许可:本文采用CC BY-NC-SA 4.0许可协议,转载请注明出处。