飞道的博客

Golang go语言速成手册与细节分析(基础篇)

609人阅读  评论(0)

适合有其它编程语言经验(最好是C)的同学,快速上手go语言的相关特性,了解go语言的运行细节,并结合许多实用的go伪代码来服务于真实场景。

GO语言简介

诞生原因

  • 目前编程语言不能合理的利用多核CPU
  • 软件系统复杂度越来越高,缺乏简洁高效。主要问题:

1.风格不统一

2.对硬件的调度,运行速度不够

3.处理大并发不够好

  • C/C++运行速度够快,但是编译速度慢,存在内存泄漏等问题需要解决。

特点

静态编译语言的安全和性能+动态语言开发维护的高效率

  • 从C中继承表达式语法,控制结构,基础数据结构,调用参数传值,保留C一样的编译执行方式及弱化的指针。
  • 引入包机制,每个.go必须属于一个包
  • 垃圾回收,内存自动回收

天然支持高并发

(1)语法层面支持并发,实现简单

(2)goroutine,轻量级线程,可实现大并发处理,高效利用多核

(3)线程间管道通信机制

(4)函数返回多个值

(5)CPS(communicating sequential process)并发模型

(6)切片slice,延时执行defer

语法规范

  • 保持语法和代码风格高度规范

go命令

  • gofmt main.go # 直接shell输出,自动修正缩进代码

参数:-w 输出重定向至文件

语法规范

  • 2 + 4 * 5 //运算符两侧加空格
  • 大括号唯一写法
func main(){

} // 正确

func main()
{

} // 错误
  • 每行超过80字符,使用","换行
  • 编译时,每行自动添加;
  • 没有用的包等语法默认必须强制删除,否则无法通过编译。

标识符命名规则

(1)英文大小写,_,0-9

(2)不能数字开头

(3)严格区分大小写

(4)不包含空格

(5)_,空标识符,仅作为占位符,本身的值会被忽略

(6)不能系统关键字

  • 驼峰法: stuName xxxYyyZzz

导出名(共有私有)

在 Go 中,如果一个名字以大写字母开头,那么它就是已导出**(共有)**的。例如,Pizza 就是个已导出名,Pi 也同样,它导出自 math 包。若首字母小写(私有)只能本包中使用。

pizzapi 并未以大写字母开头,所以它们是未导出的。

在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。

执行代码,观察错误输出。

然后将 math.pi 改名为 math.Pi 再试着执行一次。

package main

import (
	"fmt"
	"math"
) //注意是()

func main() {
   
	fmt.Println(math.Pi)
}

数据类型与变量

数据类型

整型类型
  • 默认的int,uint长度与运行的操作系统有关。
  • 转义符 T% 显示类型,unsafe.Sizeof()显示长度
  • go中的 nil = null
浮点数类型
  • 存放方式,浮点数=符号+指数+尾数
  • 浮点数都是有符号的
  • 浮点数长度固定,不受操作系统影响
  • .512, 5.12, 5.1234e2
字符串类型
  • go中没有专门的字符类型,储存单个字符(英文byte,中文rune)。
  • go的字符串由一个字节为一个单位,对比C是由一个字符为单位。(英文一个字符一个字节,中文一个字符两个字节)
  • 字符串赋值后无法任何改变。
  • 双引号识别转义字符,反引号以原生形式输出(输出源代码、防止攻击)。
  • “+”拼接字符串(长字符串换行“+”留在上一行,原因自动;)。
基本数据类型转换

不同数据类型任何情况不能自动转换(低精度到高精度等),必须使用强制转换(显式转换)。

  • 强制转换语法
var int32_i int32 = 100

var int64_i int64 = int64(i) //变量i转换为int

1.转换后int32_i还是int32,只是把转换后的值赋予int64_i。

2.高精度->低精度(int64->byte),会溢出处理。

  • int32 + 10 为int32不能赋值到int64

    var n1 int64 = 100
    var n2 int32 = 100
    n1 = n2 + 10 //n2 + 10为int32不能直接赋值int64,编译不通过
    n1 = int64(n2) + 10 //编译通过
    
  • var n1 int32 = 12
    var n2 int8 = 0
    n2 = (int8)n1 + 128 //编译不通过,128不是int8
    n2 = (int8)n1 + 127 //编译通过,但是溢出处理
    
基本数据类型与string转换
  • 基本->string

(1)fmt.Sprintf

str = fmt.Sprintf("%d" , num_int)

(2)strconv

#strconv.Itoa int快速转为str

#FormatInt

strconv.FormatInt(int64(original_int64_varialble), radix)
#第一个输入必须int64,第二个为转换的进制2-36

#FormatFloat

#FormatBool

  • string->基本, strconv

#strconv.ParseBool

b , _ = strconv.ParseBool(str)
#多返回值:bool与err,不需要的_忽略接收

#strconv.ParseInt

默认返回值数据类型为int64,需要其他int类型需要强制转换。

  • 逻辑转换错误

#若string为“hello",强制转换成int,则转换失败默认赋值为默认值0。

值类型和引用类型
  • 值类型:基本数据类型int,float、bool、string、arrauy、struct。

#变量直接储存值,内存通常分配在栈。(编译器逃逸分析可能分配在堆)

  • 引用类型:指针、slice切片、map、管道chan、接口interface。

#变量储存地址,这个地址对应的空间才是存储的值,内存通常在堆上分配,当没有变量引用这个地址时,变成垃圾被GC回收。

变量

零值

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

数值(整型、浮点型)类型为 0

布尔类型为 false

字符串为 ""(空字符串)。

多变量声明

1.var n1, n2, n3 int

2.var n, name, n3 = 1 , “tom”, 2

3.n, name, n3 := 1 , “tom”, 2 //类型推导

短变量声明
func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用

//全局变量
var{
	n1 = 1
	name = "tom"
	n2 = 3
}
作用域
  • 函数内部声明的变量为局部变量,作用域仅限于函数内部
  • 函数外部声明的变量为全局变量在整个包都有效,如果首字母大写,可以在整个程序import后跨包使用
  • 如果在if/for代码块中声明,仅在该代码块有效

运算符

  • go语言不支持三元运算符

算术运算符

除法
  • 运算结果保留,根据运算的数据类型中高精度的决定:
10 / 4 = 2 //运算的两个数都是整型,因此转换为整形,数据溢出,结果为2
var i float = 10 / 4 //还是2
var i float = 10.0 / 4 //2.5,需要浮点数参与运算
取模
  • a % b = a - a / b * b //a / b需要向下取整到整型
自增、自减
a := b++ //错误,只能单独b++使用
if i++ > 0 { //错误
  • 强制语法统一,独立使用i++,减少歧义

  • 无–i,++i

关系运算符

  • 只返回bool值

逻辑运算符

  • 返回bool值
短路与、短路或
  • && 也叫短路与,如果第一个为false,则第二个条件的代码不会运行判断,最终结果为false。
  • || 也叫短路或,如果第一个条件为true,则第二个条件不会判断,最终结果为true。

赋值运算符

  • 从右向左运算

  • =, +=, -=, *=, /=, %=

  • <<=, >>=, &=, ^=, |=,

<<= // c = c << 2 , 右移两位后赋值c
&= // c = c & 2 , 与2位与后赋值c

指针

  • **区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。**移动指针需要使用unsafe包

  • 变量名不等于数组开始指针,如果p2array为指向数组的指针, *p2array不等于p2array[0]。

  • 数组做参数时, 需要被检查长度。

结构

返回值

多值返回

1.只接受多值返回中的部分值

// 使用_进行占位
a, _, c = fun_return_abc()

2.两种返回方式

// 
func swap(x, y string) (string, string) {
	return y, x
}
//
func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

if

语法细节
  • 就算逻辑块内只有一条语句也必须有大括号。
if a > 0 {
	fmt.Println("a")
}
  • if判断条件建议不带()
  • else,else if必须不换行
if ... {
	...
} else if{
	...
} else {
	...
}
if 语句定义的变量作用域在if内

for 一样, if 语句可以在条件表达式前执行一个简单的语句。

  • 该语句声明的变量作用域仅在 if 逻辑块之内。
  • 同时在if语句判断条件中只能写判断,不能赋值等操作。
执行细节
  • 多分支控制只有一个出口,即多个if条件满足,对第一个为真的执行,执行完跳出条件判断。
if 条件1 { // 1为真
	...
} else if 条件2 { // 2为真
	...
} else if 条件3 {
	...
} else {
	...
}
//执行完1后跳出条件判断
package main

// 推荐的引入包的方式
import (
	"fmt"
	"math"
)

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else { //必须else不换行,保证这种格式
		fmt.Printf("%g >= %g\n", v, lim)
	}
	// 这里开始就不能使用 v 了
	return lim
}

func main() {
// 由内层开始执行,先pow最后println
	fmt.Println(
		pow(3, 2, 10),
		pow(3, 3, 20),
	)
}

for

执行流程

1.循环变量初始化 i++

2.执行循环条件 i <= 10

3.判断条件(返回bool值);若为真,执行循环操作(语句块1)

4.执行循环变量迭代

5.反复1234,知道循环条件False

for i := 1 ; i <= 10 ;i++ {
	语句块1
}
for 包含 while
while/do-while
func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}

for {
	if 循环条件表达式{ //决定dowhile还是while
		break
	}
	循环操作
	循环变量迭代
}
死循环

如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。

  • 搭配break设置跳出条件
使用细节
  • 循环调节返回值一定为bool
  • 可以只跟判断条件
for i < 10 {
	i++
}
  • 数组、字符串遍历
  1. 下标
for i := 0; i < len(str); i++ {
	fmt.Println(str[i])
}
  • 注意下标遍历字符串:go中全部按照utf8编码,字符串格式按照一个字节为单位划分,中文为三个字符串,因此需要str转rune再遍历。
str = []rune(str)
  1. for range方式
  • 默认转换为rune,按照字符遍历,不需要转换
for index, val := range str {
	fmt.Println(val)
}

switch

语法细节
  • 无break
switch 表达式 {

case 表达式1, 表达式2, ...:
	语句块1
	
case 表达式3, 表达式4, ...:
	语句块2
	
default:
	语句块
}
  • switch、case后可以跟常量、变量、函数的返回值

  • case后的数据类型必须和switch后的数据类型严格统一、完全一致

  • case后可以跟多个表达式

case 5, 10, n1 :
	语句块
  • 若case后为常量(只要是变量就可以),则不同的case后的常量之间不能数值重复

  • default语句块不是必备的

高级用法
  • 无条件的switch,使多重判断更简洁(类似if else的逻辑)
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

//在switch内声明变量,不推荐
func main() {
	switch t := time.Now() {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}
  • fallthrough 穿透特性
    如果在case语句后增加fallthrough,则会继续判断下一个case
case a:
	语句块1
	fallthrough //会判断下面的case
case b:
  语句块2
  • type switch

判断interface变量中实际指向的变量类型

break、continue、goto、return

break
  • 中断for,跳出switch
使用label控制跳出层数
label1: {
label2: 	{
label3: 		{
								break label1 // 若没有标签默认跳出一层,也就是跳出label3这一层,跳出最近的一层for
								// label1 等于break break break
}
}
}
  • 标签名字随意,格式固定。
continue
  • 结束本次循环,继续执行下一次循环;也可以使用label来确定是跳过那一层循环
goto
  • 无条件的转移到程序指定的行,通常与条件语句配合使用,实现条件转移,跳出循环等功能。
  • 一般主张不使用goto
return
  • 跳出所在的方法或函数。

  • 一个包=一个文件夹,本质创建不同的文件夹来存放不同的程序文件
  • go的每一个文件都属于一个包,通过包的形式来管理文件和项目目录结构
  • package、import

包的作用

  • 区分同名函数、变量等标识符
  • 当程序文件多时,构建合理的项目结构
  • 作用域,控制函数、变量等访问范围

包名与引入

  • 通常与当前.go上一级文件目录名称保持一致,一般为小写字母

  • 首字母大写(可以是变量名、函数名)为可导出(公有、可导出、跨包使用),小写(私有、不可导出)

  • 注意区分包名和包所在上级目录文件夹名称

//被调用文件,在/.../dir下
package utils //1.utils为包名,package规定打包的包名 2.package第一行

//调用文件
package use_utils
import (
	"/.../dir" //在dir路径下寻找包
)

utils.TestFun() //用包名调用函数
  • import默认路径从环境变量$GOPATH下的src开始,编译器自动从src寻找引入
  • 包名过长可以使用别名(原包名不可继续使用)
import (
	utils "/.../dir" //命名包别名为utils,后续引用**只能**使用别名,不能用原包名
)
  • 同一包下不能有相同的函数名(以包做命名空间)

  • 一个包也就是一个文件夹下可以有多个.go文件,但是都属于一个包,因此不能有相同的函数名和全局变量名

  • 如果需要编译成一个可执行文件,则需要将这个包声明为main包,其它的库文件名称可以自定义

go build -o \bin\test.exe(保存路径) \...\main(需要编译的项目目录) 
  • 引入的包必须使用,不使用如下:
import (
	"a"
	_ "b" //引入不使用
)

默认项目结构

  • src存放源码
  • package存放,编译后的库文件(类似.dll,.so)

函数

func 函数名 (形参列表) (返回值列表) {
	执行语句
	return 返回值列表
}
  • 函数中的变量是局部的,函数外不生效

  • 基本数据类型和数组都是值传递,即值拷贝。在函数内修改,不会影响到原来的值。(不同的物理地址,可能变量名相同)

  • 引用类型传递,传递的是被引用值的地址或者索引,因此修改的是同一个物理地址(指针),但是地址本身还是值传递

  • 严格不允许函数名称重名(不支持函数重载)

  • 在go中函数也是一种数据类型,可以赋值给一个变量,则该变量是一个函数类型变量,通过此变量可以调用函数

func myFun(funvar func(int, int) int, num1 int, num2 int) {
    //funvar是函数变量形参名字
	return funvar(num1, num2)
}
  • 为了简化数据类型定义,go支持自定义数据类型(与typedef很类似,别名);一般在程序import后就type,在函数中局部生效
type mySum func(int, int)int //mySum等价于func(int, int)int函数数据类型
//注意在虽然本质都是同一种类型,但是编译器认为两个是两种数据类型
  • 函数返回值命名
//主要作用是返回时按照函数返回值列表顺序接收
func test(n1 int, n2 int) (n3 int, n4 int){
	n3 = n1
	n4 = n2
	return //直接return,按照
}
n3, n4 = test(1,2)
  • 使用"_"作为占位符,忽略返回值
  • 支持可变参数(动态参数个数);如果有可参数需要将可变参数放在形参列表最后
//支持0到多个参数
func sum(arg... int) sum int { //arg为标识符,可以是其它名称
}
//支持1到多个参数
func sum(n1 int, arg... int) sum int {
}

//求一个到多个int的和
func sum(n1 int, vars_int... int) int {
	sum := n1
	for i :=0; i < len(vars_int); i++ {
		sum += var[i]
	}
	return sum
}

内存分区与形参实参

  • 栈区(通常存放基本数据类型)、堆区(通常存放引用数据类型)、代码区(存放代码)、全局区、常量区…(go编译器存在逃逸分析机制)

  • 栈区函数结束释放,堆区当这个引用类型没有任何变量引用这个地址被GC回收

  • 运行一次函数在栈区中拥有一个命名空间(下面的demo适用于栈区的基本基本数据类型)

func test (n1 int) { //使用栈区中test栈区中的n1来接收
	n1 = n1 + 1
	fmt.Println(n1) //输出11
}

func main { 
	n1 := 10 //在栈区中的main栈区创建n1
	test(n1) //main栈区n1对test栈区n1赋值,实参对形参赋值
	fmt.Println(n1) //调用完毕,回收test栈区,输出10
}

栈区 ------ test栈区
       |
       --- main栈区
       |
       --- ...

递归调用

  • 每一次调用都会为test生成一个新的栈,最后逐层回收
  • 局部变量相互独立,只是名字相同
func test(n int) {
	if n > 2 {
		n--
		test(n)
	}
	fmt.Println("n=" , n)
}

//输出 2 2 3

func test(n int) {
	if n > 2 {
		n--
		test(n)
	}else{
	fmt.Println("n=" , n)
	}
}

//输出 2

init函数

  • 每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被go运行框架调用。

  • 文件有全局变量定义:变量定义->init->main

  • init主要作用:完成初始化工作

  • 引入的包有init时完整执行顺序:

package main //1
import ( //2
	"/.../utils"
)

func init() { //6

}

func main() {  //7

}

package utils //3

func init() { //4

}

func main() { //5

}

匿名函数

  • 如果我们某个函数只希望使用一次,可以考虑匿名函数,匿名函数也可以多次调用
使用方式
  • 定义匿名函数时直接调用(只能调用一次,使用较多)
func main() {
	res1 := func (n1 int, n2 int) int {
		return n1 + n2
	}(10, 20)
}
  • 将匿名函数赋值给一个变量,再通过变量调用匿名函数(可以多次使用,使用频率低,意义:可以在函数内定义函数)
func main() {
	func_name := func (n1 int, n2 int) int {
		return n1 + n2
	}
	res1 := func_name*(10, 20)
}
全局匿名函数
  • 将函数赋值给全局变量,则可以在整个程序中通过全局变量调用函数

闭包

  • 闭包就是一个函数和与其有关的引用环境组合的一个整体(本质是一个函数)
  • 闭包=函数返回函数+函数内被使用的变量
// 累加器
func AddUpper() func (int) int {
// 闭包开始
	var n int = 10
	return func (x int) int { 返回的是一个匿名函数,但是函数引用了函数外的n,因此匿名函数和n形成了一个整体,构成了闭包
		n = n + x
		return n
	}
// 闭包结束
}

func main() {
	f := AddUpper()
	fmt.Println(f(1)) // 10 + 1 = 11
	fmt.Println(f(2)) // 11 + 2 = 13
	fmt.Println(f(3)) // 13 + 3 = 16
}

// 返回的数据类型是fun(int)int 
// 关键是返回的函数使用到了那些变量,函数和它使用的变量构成闭包
demo分析
  • 闭包的优势:向函数传递一次参数,参数可以被持久化记录,不用每次调用都反复传入参数;当函数的部分参数需要短暂持久化(但是需要有接口可以修改),使用闭包
// 需求:1.判断文件名是否有.后缀,若有返回直接文件名(直接返回name.xxx),若无加上.xxx返回(name.xxx)
				2..xxx可以修改即可
			  3.被调用的时候,大部分时间后缀使用.jpg
func MakeSuffix (suffix string) func (string) string {
	return func (name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		} else {
			return name
		}
	}
}

f := MakeSuffix(".jpg")
f("1") //返回1.jpg
f("2.jpg") //返回2.jpg
//闭包 = 匿名函数 + suffix

defer

  • 需要创建资源(数据库连接、文件句柄、锁等),但是需要在函数执行完毕后及时释放资源 ,因此提供defer(延时机制)
一个值defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

package main

import "fmt"

func main() {
  //当执行到defer语句时,会语句将压入defer栈(独立栈)
  //**当函数执行完毕后,按照先入后出的方式出栈执行**
	defer fmt.Println("!") //3
	defer fmt.Println("world") //2

	fmt.Println("hello,") //1
}

func main() {
  n1 := 1
  n2 := 2
  // 在defer语句入栈时,会将相关的**值拷贝**(和原变量为两个独立的变量)同时入栈
	defer fmt.Println(n1) //1
	defer fmt.Println(n2) //2
	
	n1++
	n2++
	
	fmt.Println(n1+n2) //5
}

func test() {
	file = openfile()
	defer file.close() //不用考虑具体啥时候关闭文件句柄
  ...
}
多个值defer
package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

函数参数的传递方式

值类型默认使用值传递,引用类型默认使用引用传递

如果希望函数外修改函数内变量,只能引用传递,利用&以指针的方式操作变量

  • 值传递
地址1       地址2
|     ->    |
值1         值2

拷贝效率低

值类型:基本数据类型int、float、bool、string,数组,结构体

  • 引用传递
地址1 -> 地址2
|        |
值1      值1

拷贝效率高

引用类型:指针、切片、map、管道chan、interface等

常用系统函数

buildin,内建函数不用import直接使用

字符串函数
  • len(str),按字节统计字符串长度或数组长度(go使用utf8,ascii字符(字母和数字)占一个字节,一个汉字(或者其它语言)占用三个字节)

  • 中文字符串遍历,r := []rune(str)

  • 字符串转整形,n, err := strconv.Atoi(""),可以利用err判断输入是否是纯数字;整数转字符串,str = strconv.Itoa(),不存在不能转换

  • 字符串转byte,转为ascii,var bytes = []byte(“str”);byte转字符串(ascii),str = string([]byte{97, 98, 99})

  • 10进制转2、8、16进制,str = strconv.FormatInt(123, 2)

  • 查找子串是否在指定字符串,strings.Contains(“abcddd”, “abc”) // true

  • 统计字符串由几个指定的子串,strings.Count(“abcddd”, “ddd”) // 3

  • 不区分大小写比对字符串,strings.EqualFold(“str”, “str”);不区分大小写,str == str

  • 返回子串第一次出现的index,若无返回-1,strings.Index(“adcddd”, “ddd”) // 3;返回子串最后一次出现的index,若无返回-1,strings.LastIndex(“adcddd”, “ddd”)

  • 使用子串替换另一个子串,n为第n个,n=-1表示全部替换,strings.Replace(“abcddd”, “ddd”, “eee”, n),可以是变量传入

  • 按照指定字符拆分字符串,strings.Split(“1,2,3,4”, “,”),返回一个字符串数组

  • 大小写转换,全部变为大写或小写,strings.ToLower(str),strings.ToUpper(str)

  • 去掉字符串两边的所有空格,strings.TrimSpace(" test ")

  • 将字符串左右两边指定的字符去掉,不考虑去除去字符串的顺序,strings.Trim("!ahelloa!", “!”) // hello

  • 将字符串左(右)侧指定字符去掉,不考虑去除去字符串的顺序,strings.TrimLeft("!hello!", “!”)、strings.TrimRight("!hello!", “!”)

  • 是否以指定的字符串开头、结尾,strings.HasPrefix(“abc”, “a”)、strings.HasSuffix(“abc”, “c”)

日期和时间函数

内置函数(buildin)

异常处理

  • go追求简洁优雅,不支持try…catch.finally

  • go引入的异常处理方式:defer、panic、recover

  • go抛出一个panic异常 -> 在defer中的recover中捕获这个异常 -> 处理异常

func test() {
	defer func () {
		err := recover() // 捕获异常,将异常返回err变量(error类型)保存
		... // 处理异常
	}() // 匿名函数,无输入,无输出
	... // 此处抛出错误
	... // 跳过错误,抛出异常后继续执行
}

func test() {
	defer func () {
		if err := recover() ; err != nil { // 捕获 + 判断
			...
		} // 捕获异常,将异常返回err变量(error类型)保存
		... // 处理异常
	}() // 匿名函数,无输入,无输出
	... // 此处抛出错误
	... // 跳过错误,抛出异常后继续执行
}
自定义错误
  • errors.New(“错误说明”)创建自定义错误,返回error类型的值
  • panic,接受一个interface{}类型的值(可以是任何 值)作为参数,接收error,输出error信息,退出程序
// 读取配置文件,目录下有无指定文件名文件
func readConf(name string) (err error) {
	if name == "config.ini" {
		return nil
	} else {
		return errors.New("file name error!")
	}
}

func test() {
	err := readConf("...")
	if err != nil  {
		panic(err)
	}
	...
}

数组和切片

数组

  • 在go中数组时值类型,arrayName为变量名(a),数组为变量数据(1)。(var a int = 1)

  • 创建数组后自动为每个数组元素赋零值(详情见零值章节)

  • arrayName地址为数组首地址(=第一个元素的地址),arrayName是值类型,直接存放数组(数组看做一个和int等一样的值);数组地址为:首地址(变量arrayName地址、第一个元素地址)+ n * 数组类型长度(例如:int64,为8个字节,地址加8)

  • 数组时多个相同数据类型的数据的组合,数组一旦声明,其长度就固定,不能动态变化。(数组长度也是数组信息的一部分)

  • 注意切片和数组的区别,var arr []int 这种写法arr是切片

  • 数组可以是基本数据类型也可以是引用类型,但是只能全部元素是一种数据类型

  • 数组下标从0开始

  • go数组属于值类型,在默认情况下进行值传递,因此进行值拷贝不会影响原数组

  • 想函数外修改函数,需要传递指针,使用引用传递

初始化数组的方式
  • 定义数组:var arrayName [n]float64;访问、赋值数组元素;arrayName [0] = …
  • 使用顺序:开辟空间->赋值->访问
var array [3]int = [3]int{1, 2, 3}
var array = [3]int{1, 2, 3}
var array = [...]int{1, 2, 3} // ...系统自动判断大小
var array = [...]int{1: 2, 0: 1, 2: 3} // 数组下标+数据
array := [...]int{1, 2, 3} // 类型推导,最简洁
遍历
  • 常规遍历
for i := 0; i < len(array); i++ {
	...
}
  • for-range遍历
for index, value := range array {
   
	...
}

// 1. 第一个返回值index是数组下标
// 2. 第二个value是在该下标位置的值
// 3. index、value都是在for内部可见的局部变量
// 4. 若遍历数组时不需要index或value,使用_替代
// 5. index和value是两个变量接受返回值,名称不固定,习惯使用index和value
二维数组
var arr [n1][n2]int //n1,n2为数组真实大小,不是从0开始
arr[n3][n4] = i //n3,n4为索引从0开始计数
&arr[0][0] //第一个一维数组首地址
&arr[1][0] //第二个一维数组首地址

var arr [n][n]int = [...][n]int{
  {...},{...}} // 只有一维数组也就是第一个位置可以...,因为是按照初始化时的个数来自动确定个数
var arr = [n][n]int{
  {...},{...}} 
  • 底层寻址方式和一维数组一致,数组元素变为数组指针;也就是二维数组保存着n个一维数组的首地址,然后通过一维数组的首地址访问一维数学组。
遍历
for i := 0; i < len(arr); i++ {
	for j := 0; j < len(arr[i]); j++ { // 注意len(arr[i])
		...
	}
}

for i1, v1 := range arr {
	for i2, v2 := range v1 { // 注意range v1
		
	}
}

切片

  • 切片是数组的引用(修改slice,原数组也会改变),切片的长度可以变化,是一个动态的数组。(底层追加的方式是1.有容量的概念存在一定的冗余;若冗余不足,2.拷贝原切片指向的数组的值到一个新的空间,然后创建切片返回新的切片,然后通过切片变量接收)

  • 数组a[1](有确定大小),切片a[](无确定大小)

  • 与数组相比,遍历、访问元素(silce[n])、len()用法相同

  • 定义:var sliceName []int

  • 切片有容量的概念(容量可以动态变化),也就是目前能存放最大元素的个数,使用cap()函数可以查看;一般容量是元素个数的两倍

  • 切片本质是一个结构体:1.首个元素的首地址 2.长度 3.容量\

  • 切片必须定义后,赋值后再使用

  • 切片可以引用切片(或其它引用变量)

  • 在初始化时不能越界,但是可以动态增长。可以使用append函数动态追加元素

  • append、copy的所有对象只能是切片,不能是数组

slice3 = append(silce3, 400, 500, 600) // append的方式是创建一个新的切片(数组),然后返回切片通过赋值传递给切片
slice3 = append(slice3, slice1...) // 切片追加切片
使用方式
  • 定义一个切片,通过引用的方式去引用已经创建好的数组;数组已经创建,再创建切片使切片引用数组
  • 简写:arr[0:end],arr[:end];arr[start:len(arr)], arr[:end];arr[0:len(arr)],arr[:]
var array [3]int = [3]int{1, 2, 3}
silceName := array[0:2] // 引用array数组起始下标为0,终止下标为2**(不包含2)**
  • 通过make函数(还可以创建映射和通道)创建切片,此时切片还是结构体,指向一个数组空间(但是数组匿名无法通过数组名访问),只能通过切片访问。数据结构模型不变:切片(结构体)->数组空间(数组匿名或数组名可访问)
  • make创建切片的同时创建一个数组(空间),数组底层由切片维护,数组无法通过数组的正常方式访问
var slice_name []type = make([]type, len, cap(可以忽略))
var slice []int = make([]int, 4, 8)
slice[0] = 100
  • 直接定义
var silce_name []int = []int{1, 2, 3}
遍历
  • for循环常规遍历
for i := 0; i < len(slice); i++ {
	...
}
  • for-range
for i, v := range slice {
	... //i为index,v为value
}
拷贝
  • 拷贝和被拷贝的两个数据空间是独立的

  • copy(slice1, slice2) // slice1内容拷贝到slice2,若容量1>2优先数组下标0开始,剩下的不拷贝;2>1从数组下标0开始拷贝,2多余剩下的内容不变

string和slice
  • string也是一个结构体:1.数组首地址、2.长度,底层指向一个byte数组;因此string可以使用切片操作,slice也指向byte数组
// 数组截取
str := "hello,world!"
str_slice := str[7:] // world!
  • 使用slice修改字符串,
// 错误
str[0] = 'z' // str本质是字符串结构体,无法通过数组的方式直接寻址,修改字符串

// 将string->[]byte/[]rune(byte或rune切片),然后修改
arr1 := []byte(str) 
/*
1.如果处理中文需要转为[]rune
2.原因:go中string为byte数组(以字节为单位),go使用utf-8,ascii内的字符(英文、数字等)一个字节,其他语言(中文等)三个字节,因此要使用rune
*/
arr1[0] = 'z'

map(映射)

  • key-value
  • var mapName map[keytype]valuetype;var name map[int]string
  • 声明后(未分配内存,数组会直接分配内存,赋零值),再make(分配内存),再使用,可以动态调整大小
//
var mapName map[int]string
mapName = make(map[int]string, 10)

//
var mapName = make(map[int]string, 10)

// 底层自动make
var mapName map[int]string = map[int]string{1:"no1", 2:"no2", } // 注意,
mapName := map[int]string{
	1:"no1", 
	2:"no2", // 注意,
}
  • key唯一,如果出现重复的key赋值,会覆盖同名的key-value,同时无序
  • key-value在map中存放是无序的(与key大小、添加顺序无关),可能每次输出的顺序都不一样
  • len(map),一个map有len(map)对键值对
  • map是引用类型的数据,传递时函数外可修改
  • map自动扩容(动态增长),不会报错;切片必须手动append扩容,否则panic错误
  • map的value是结构体最好,可以管理复杂的数据

key

  • key的数据类型可以是:bool、数字、string、channel,还可以是以上几个类型的接口、结构体、数组
  • 不能是:slice、map、function,因为无法用==判断
  • 通常是:int、string

value

  • 通常为数字、string、map、struct
  • 嵌套map,value为map
// 一个学生信息包含:name、sex、address

//为最外层map分配空间
studentMap := make(map[int]map[string]string) // 不输入大小默认为1
// 为具体的学生分配空间
studentMap[1] = make(map[string]string, 3)
studentMap[1]["name"] = "..."
studentMap[1]["sex"] = "..."
studentMap[1]["address"] = "..."

增删查改

  • 增、改
map["key"] = value // 如果key还没有就增加,如果已经存在就更新(也就是修改)
  • 删除
delete(map, "key") // 如果key不存在,不会报错,也不会操作

// 如果全部删除所有的key,推荐2
// 1. 遍历key
// 2. map=make(),make一个新的同名map,让原来的成为垃圾被gc回收
  • 查找
val, res := map[“1”] // val储存返回键值,res返回是否存在键值,true存在,false不存在

遍历

  • 只能使用for-range遍历
for k, v := range map {
	...
}

切片

  • 数组元素是map的动态数组
var mapSlice []map[int]string // 定义map切片
mapSlice = make([]map[int]string, 2) // 为slice分配内存
mapSlice[0] = make(map[int]string, 2) // 为map元素分配内存

// 在slice中追加map
newMap := map[int]string {
	...
}
mapSlice = append(mapSlice, newMap)

排序

  • key排序,key无序,每次输出可能顺序都不一样

  • 排序思路,先将map的key放入切片,对切片排序,遍历切片,按照key输出map

var keys []int
for k, _ := range map {
	keys = append(keys, k) //遍历插入key切片
}
sort.Ints(keys) // 使用自带方法排序
for _, k := range keys {
	map[keys] //按照key的顺序遍历输出
}

转载:https://blog.csdn.net/viafcccy/article/details/117073708
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场