Go语言学习(面向区块链)

第一个Go语言程序

gopath目录

gopath目录就是我们存储我们所编写源代码的目录.该目录下有三个字目录:src, bin, pkg.
src--->里面每一个子目录,就是一个包.包含Go的源码文件
pkg--->编译后生成的,包含目标文件
bin--->生成可执行文件

GOPATH环境变量
把我们自己存放go语言的目录告诉计算机就可了
新建一个环境变量,然后告诉计算机
步骤
1.新建一个文件加,'goprojects',这个就是存放我们自己开发的程序
2.仿照go官方目录,创建'src(目录,用于存放go语言程序的源码)' 
				  'pkg(目录,用于存放引用其他第三方的程序代码)' 
				  'bin(目录,用于存放编译后的二进制文件(可执行文件))'

main函数和init函数

  1. init函数是用于程序执行前做包的初始化函数,比如初始化包里的变量等
  2. 每个包可以有多个init函数
  3. 包里的源文件也可以多个init函数
  4. 同一个包中多个init函数的执行顺序go语言没有明确的定义
  5. 不同包的init函数按照包的包导入的依赖关系决定该初始化函数的执行顺序
  6. init函数不能被其他函数调用,而是在main函数执行前,自动被调用

main函数和init函数异同

相同点:

​ 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用

不同点:

​ init可以在任意包,且可以定义多个

​ main函数只能在main包,只能定义一个

由于Go语言允许将一个包的代码分散在多个代码文件中(前提是这些文件必须处于同一级目录下),在编写代码时就有可能出现main函数重复定义的问题,显然,在一个应用程序中,main函数只允许出现一次.

做法:…

运算符

1.操作数

若某个对象与运算符一起出现并参与运算,便可以称这个对象为操作数.操作数与运算符一起组成了表达式

2.算术运算符

2.1四则运算符

package main
import ("fmt")

func main(){
    //声明两个变量m,n,类型为int,用于存放操作数
    var m, n int
    
    fmt.Print("请输入第一个操作数:")
    fmt.Scan(&m)
    fmt.Print("请输入第二个操作数:")
    fmt.Scan(&n)
    
    //Scan函数会从标准输入流读取数据,然后存放在m,n变量中.这里采用引用传递参数(加上&符号,获取变量内存的地址),以保证函数调用后m,n变量能引用到所读的内容
    
    r1 := m + n
    r2 := m - n
    r3 := m * n
    r4 := m / n
    
    //使用 := 运算符可以直接向新变量赋值,不需要var关键字声明
    
    fmt.Printf("%d + %d = %d\n", m, n, r1)
    
}

2.2取余运算符

%

2.3指数运算

math包中有两个函数可以用于指数运算

Pow函数的声明如下:

func Pow(x, y float64) float64 其中,参数x为底数,y为指数.即x ^ y.此外,还有一个Pow10函数:

func Pow10(n int) float64 10 ^ n.

//计算5的立方
result := math.Pow(5, 3)
fmt.Printf("5的3次方: %d\n",int(result))

注意:

Pow与Pow10函数的返回值类型为float64类型,调用Printf函数输出格式化字符串,如果使用%d格式控制符,则需要先把计算结果转换为int类型,再传递给Printf函数,如果使用%f格式控制符,则不需要类型转换

2.4自增与自减运算符

3比较运算符

4逻辑运算符

5位运算符

5.1按位与

var (
	m int8 = 12
	n int8 = 6
   )

将变量m, n进行与运算
result := m & n
1 1 0 0       //12
0 1 1 0       //6
---------------------
0 1 0 0       //4

5.2按位或

var a uint8 = 220
var b uint8 = 89
var c = a | b
1 1 0 1 1 1 0 0       //220
0 1 0 1 1 0 0 1       //89
----------------------------
1 1 0 1 1 1 0 1       // 221

5.3取反

取反运算符(^)可反转二进制位的值,0–>1,1–>0

100101

取反

01101

无符号整数

var n uint8=27     00011011
var r = ^n         11100100 -->228
27+228=255,即uint8数据类型的最大值

var g uint16=150   0000000010010110
var q = ^g         1111111101101001  -->65385
150+65385=65535,即uint16数据类型的最大值

有符号数

通用公式:
c = -(n + 1)
7 ----> -8
-6 ---->  5

5.4位移

10011101 >>3  结果:00010011
10101001 <<2      10100100
另外注意有符号位的移位

5.5按位异或

不相等返回1,相等返回0
var x uint8 = 0b_1011_1101           1 0 1 1 1 1 0 1
var y uint8 = 0b_1110_0001           1 1 1 0 0 0 0 1
r = x ^ y                           ------------------
									 0 1 0 1 1 1 0 0

5.6清除标志位

清楚标志位,就是将特定的二进制位的值变为0.其运算符是 &^

例如,要将11011111最右边三位变为0,代码如下:
var k uint8 = 0b_11011111
var r = k &^ 0b_00000111
结果为:11011000

6成员运算符

成员运算符就是(.),也称"筛选"运算符,用于访问某个对象的成员

成员运算符可以访问各种类型对象的成员,但要注意以下情况:

(1).如果对象是指针类型或者接口类型,并且它的值是空引用(nil),那么对此成员的访问会发生错误

程序包管理

1.package语句

2.程序包的结构

​ Go语言是以目录为单位来界定程序包的,因此,在同一级目录下只允许使用一个包名.一个包则可以分布在多个代码文件中

3.导入语句

import <包所在的路径>
package main

import (
	."GoProjects/src/demo/test01"   // 如果被导入包内的成员不会与当前代码的成员名称冲突,还可以直接把某包中的成员名称合并到当前文件中.
									// 实现方法用句点(.)作为被导入包的别名
									
	mu "GoProjects/src/demo/test02"  // 如果觉得包名太长,输入时不方便,可以起别名
)
func main() {

	StartPlay()
	StopPlay()

	mu.Play()
	mu.Pause()

}

4.初始化函数

在程序包代码中,可以定义一个名为init的函数,当此包被其他代码导入时,init函数会被调用

init函数的功能仅限于初始化工作,例如给变量赋值.因此,不应该在init函数写过于复杂的代码,尤其是一些消耗时间的代码.

x.go

package test

import "fmt"

func init()  {
	fmt.Println("part1 - 初始化")
}

y.go

package test

import "fmt"

func init()  {
	fmt.Println("part1 - 初始化")
}

main.go

package main

import _ "GoProjects/src/test"   //使用import语句导入test包时,为其分配一个由下划线作为别名.此做法的作用:test包仅仅执行初始化代码,而不会导入其成员.此方法会使init函数被调用

func main() {
	

}
image-20211116233335950

5.模块

若将项目代码放在GOPATH目录(源码位于src子目录下)之外,在使用import语句导入包时,可以使用相对路径
import ../test       //父级目录下的test目录
import ../../test    //父级的父级目录下的test目录
import ./test        //当前目录下的test目录

这种方法不便于管理,一但项目中的结构改变,import语句均需要修改

5.1 go.mod文件的基本结构

5.6 成员的可访问性

Go语言通过成员名称的首字母来决定其可访问性.只有成员名称的首字母为大写时,其他包中的代码才能访问该成员

package abc

func Min(a, b int) int{

}

func min_it(a, b int) int {

}

Min函数可以被abc外包访问

min_it函数只能在abc内部包访问

以下两个结构体也是一样的规则:

type person struct{

}

type Person struct{

}

person结构体只能在当前包内部访问,Person结构体就…

对于结构体的字段成员,首写字母决定其访问性的规则同样适用

golang fmt格式“占位符”

https://studygolang.com/articles/2644

变量与常量

1.变量的初始化

​ (1).声明阶段

var <变量名> <变量类型>

var s string

​ (2).赋值阶段(变量声明后,应用程序会自动为其分配一个0值,对于字符串而言,默认值是nil)

s = "你好"

s = "早上好"
s = "中午好"

可以对变量s的值进行不限次数的修改,只有最后一个值会被保留

声明时同时赋值

var b int16 = 680 此时,变量b的值为16位整数值680

也可以省略变量类型,由赋值内容自动推断变量类型

var c = 3.14159

程序将自动分析出变量c的值为float64.若担心有误,可以在赋值时进行明确的转换

var d = float32(3.14159)

或者

var d float32 = 3.14159

另外,还有一种更简便的写法,声明变量并初始化

<变量名>  :=  <变量值>

f := "xyz"   --->string
z := 1.5e7   --->float64

2.组合赋值

var a,b,c = 10,20,30
简约写法:
x, y, z := "test", 5, 0.02

调用函数接收多个返回值:

func test() (string, string, int){
	return "abc","xyz",10000
}

调用函数:

r1,r2,r3 := test()
fmt.Println("函数的返回值为: ")
fmt.Printf("r1: %v\nr2: %v\nr3: %v\n", r1,r2,r3)

3.匿名变量

如果将变量命名为"_"(单个下划线),那么他就是匿名变量.赋给匿名变量的值会被丢弃,因为他在代码中无法访问.

值"opq"将会被丢弃

a,b,_ := “abc”,“lmn”,“opq”

随后的代码只能访问变量 a 和 b

4.常量

声明常量必须使用const关键字,初始化方法与变量相同

const Val1 int = 0

const Val2 int = 1

const Val3 string = “SPEED”

const Val4 bool = false

变量在其生命周期可以被修改,但常量一旦初始化是不允许修改,

常量的声明也可以省略类型标识,让程序根据初始化的值来进行自动推断 const LockMode = -1

5.批量声明

//declare three variables     var关键字 +" "+ (变量声明)
var(
	k = 0.0001
	j = 0.0021
	m int16 = 5530
	)

//declare three constants
const (
	XldFirst = "F"
	XldSecong = "G"
	XldThird = "H"
	)

6.变量的作用域(生命周期)

变量的作用域都属于同一个包下,因此可以跨文件访问变量.

代码在访问变量时会"由远及近"的原则

如果不同层次的作用域中存在名称相同的变量时,距离当前代码较近的变量会覆盖距离较远的变量

package main

import "fmt"

var x = "EFG"

func main(){
	//此处覆盖外部变量x
	var x = "XYZ"
	fmt.Println(x)
}

7.变量的默认值

int8 0

int16 0

int32 0

int64 0

float32 0

float64 0

string

rune 0

结构体 {m:0 n:0}

接口 nil(空引用)

指针 nil(空引用)

基础类型

1.字符与字符串

与文本相关的数据类型有两个: rune 和 string.rune只能表示单个字符,string可以表示多个字符,称为字符串

1.1rune类型

在Go语言中,他表示单个字符.对于Unicode字符,例如单个汉字,也可以由rune类型表示

var x1 rune = ‘f’

var x2 = ‘G’

var x3 = ‘好’

var x4 rune = ‘@’

var x5 rune = ‘7’

rune常量表达式必须在一堆英文单引号之中,但如果要包含单引号本身,那就需要进行转义(“”),即

var x7 = ‘’’

从builtin包的源代码中能看到,rune类型的声明代码如下:

type rune = int32

这表明rune类型实际上是32为整数的别名.

rune类型不能赋值多个字符,下面的代码会发生错误

var d4 rune = ‘abc’ (x)

1.2string类型

字符串表达式需要写一对双引号(英文双引号),例如

var st string = “zyx”

若自身包含双引号,应当进行转义

var sc = “My name is “TOM””

字符串对象可以包含不定个数的字符,例如:

	1. 包含 0 个字符: 空字符串
	1. 包含 1 个字符: 与rune类型表达式相同,但数据类型不同
	1. 包含一个以上的字符:

双引号一般用于简单的字符,对于较为复杂的"段落式"字符串,可以使用"`"字符来表示

var sd = `---------------------This is title

…-2021-11-28

-----------------------------This is bottom

`

结果是"`"符号可以让字符串"原封不动"地输出,换行缩进均被保留

2.数值类型

类型 描述 范围 示例
int8 8位有符号的整数 -128~127 -1, 50
uint8 8位无符号的整数 0~255 128
int16 16位有符号的整数 -32768~32767 -321
uin16 16位无符号的整数 0~65535 20005
int32 32位有符号的整数 -2147483648~2147483647 -15000,3500005
uint32 32位无符号的整数 0~4294967295 857857857
int64 64位有符号的整数 -9223372036854775808~9223372036854775807
uint64 64位无符号的整数 0~18446744073709551615
int 有符号整数,至少32位. 在32位处理器为32位整数 在64位处理器为64位整数 32位处理器与int32类似 64位处理器与int64类似
uint 无符号整数,至少32位. 在32位处理器为32位整数 在64位处理器为64位整数 32位处理器与int32类似 64位处理器与int64类似
byte uint8的别名,8位无符号整数 0~255
float32 32位浮点数 符合IEEE-754标准
float64 64位浮点数 符合IEEE-754标准
complex64 64位的复数 复数的实部与虚部皆为 float32 数值
complex128 128复数 复数的实部与虚部皆为 float64 数值

2.1获取数值类型占用的内存的大小

单位是字节

package main

import (
		"unsafe"
		"fmt"
		)
		
 var ( 
 		n1 uint8 = 122
 		n2 uin16 = 2000
 		n3 uin32 = 53530020
 		n4 uint64 = 4.33e+5
 		n5 uint = 99977723
		 )
		 
//调用unsafe包中的Sizeof函数,获取变量所占用的内存大小

fmt.Printf("8位无符号整数: %d\n",unsafe.Sizeof(n1))     //1
.                                                     //2
.
.

2.2整数常量的表示方式

制式 说明 示例
十进制 257_6888_453
二进制 "0b"或"0B"前缀 0b_10001_1111
八进制 "0o"或者"0O"前缀 0o7707
十六进制 "0x"或者"0X"前缀,对应字母大小写 0x5c0a6b,0X39E4D

为了便于阅读,可以分段"_",但是不能出现在数值的开头,也不能出现在结尾

2.3科学计数法

2.4复数

var ca complex128 = 50 + 2i

var cb complex128 = -0.05 - 3i

var cc complex64 = 1.0001 + 0.0005i

var cd complex64 = -300 + 12i

虚部用"i"表示,但是不能大写

3.日期与类型

与日期/时间相关的API都封装在time包中,使用前需要导入包

import “time”

package main

import (
		"fmt"
		"time"
		)

func main(){
    var n = time.Now()
    fmt.println(n)
}

golang 使用 iota

iota是golang语言的常量计数器,只能在常量的表达式中使用。

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

使用iota能简化定义,在定义枚举时很有用。


1.Month类型

Month类型实际上是以int为基础定义的新类型.time包公开了以下常量,表示一年中的十二个月

const (

​ January Month = 1 + iota

​ February

​ March

​ April

​ May

​ June

​ July

​ August

​ September

​ October

​ November

​ December

​ )

//2021年12月2号 22:34:01
var thedate = time.Date(2021,time.December,2,22,34,1,0,time.Local)

var n = time.August

fmt.Printf(“n : %d\n”,n)

运行结果:

n : 8

2Weekday类型

基于int定义,表示一个星期的一天

const (

​ Sunday Weekday = iota

​ Monday

​ Tuesday

​ Wednesday

​ Thursday

​ Friday

​ Saturday

)

3Duration类型

Duration类型代码声明如下:
type Duration int64
以64位整数为基础.		"Duration"表示的是"时间段"------两个时间点之差
Duration类型以纳秒为单位
const (
	Nanosecond Duration = 1
	Microsecond = 1000 * Nanosecond
	Millisecond = 1000 * Microsecond
	Second =      1000 * Millisecond
	Minute =      60 * Second
    Hour =        60 * Minute  
)

eg:
a := 25 * time.Second   // 25秒

  1. Hours方法
  2. Minutes方法
  3. Seconds方法
  4. Milliseconds方法
  5. Microseconds方法
  6. Nanoseconds方法

4Time类型

精度为纳秒

  1. Now方法: 获取当前系统时间,返回Time实例 var ct = time.Now()
  2. Date: 通过向函数传入参数来初始化Time实例

var now = time.Now()

year, month, day := now.Date()

hour, minute, second := now.Clock()

Time类型支持时间差运算

var theTime = time.Date(2021,12,2,0,0,0,0,time.Local)

//30h之后

var newTime1 = theTime.Add(30 * time.Hour)

//四天之前

var newTime2 = theTime.Add(-4 * 24 * time.Hour)

5Sleep函数

调用Sleep函数会使当前协程(Go routine)暂停执行,并等待一段时间,然后恢复执行,等待时常由传递给Sleep函数的参数决定,类型为Duration

如果传递的参数值为0或为负值,Sleep函数就会立刻返回,不会等待

//等待3s

time.Sleep(3 * time.Second)

6Timer类型(待理解)

Timer是一种特殊的计时器,当指定的时间到期后,会将当前时间发送到其C字段中.C字段是只读的通道类型(<-chan Time),其他协程(Go routines)将通过C字段接收Time实例(计时器过期时所设置的时间)

Time类型的典型用途是在异步编程中处理操作超时的行为.C字段可视为单个事件的"信号灯",所有等待信号的协程会被阻止,直到C字段中读到Time实例为止

package main
import ("fmt"
		"math/rand"
		"time")

func main()  {
	//创建新的Timer实例
	var timer = time.NewTimer(5 * time.Second)
	//创建一个通道实例,用于标识任务已完成
	var completed = make(chan bool)
	//退出main函数时关闭通道
	defer close(completed)


	//在新的协程执行任务
	go func()  {
		//随机生成任务所需的时间
		//作用是模拟任务所消耗的时间
		rand.Seed(time.Now().Unix())
		var thelong = rand.Intn(10)
		//暂停当前协程
		time.Sleep(time.Duration(thelong) * time.Second())
		//发送信号,表示任务已完成
		completed <- true
	}()

	//判断任务是顺利完成了还是超时了
	select {
	case <- completed:
		fmt.Println("任务已完成")
	case <- timer.C:
		fmt.Println("任务超时")
	}

}		

函数

1.函数的定义

func AreaFun(w, h int) int {
	return w * h
}
//无输入参数,有返回值
func getANumber() float32 {
	return 0.00121
}

//有输入参数,无返回值
func setInt(x int){
	...
}

//无输入参数,无返回值
func hello(){
	...
}

格式:

func <函数名称> ([参数列表]) ([返回值列表]){

​ //内部代码

}

注意点:

  1. func关键字是必须的,不能省略
  2. 函数名称一般由字母与数字组成,但不能以数字开头
  3. 输入参数列表是可选的,如果没有参数,也要保留一对小括号
  4. 返回值列表是可选的,若无返回值,可以省略
  5. 函数体写在一对大括号内,与普通代码无异

2.调用函数

[变量列表] = <函数名称> ([参数列表])

  1. 变量列表用于接收函数的返回值
  2. 参数列表根据参数的定义依次传值即可
func add(x, y int16) int16{
	...
}

调用如下:

var result = add(19, 54)

3.return语句

在函数体中,使用return语句可以跳出函数,并把代码执行权返回给调用者.对于无返回值的函数,函数最后的return语句可以省略

4.多个返回值

Go语言支持函数多个返回值

func getThreeInt() (int, int, int){
	return 100, 1001, 2002

调用:

var a, b, c = getThreeInt()

var a,_,_ = getThreeInt()  //only need the first value

如果返回值已命名,可以选择性地为他们赋值(未赋值将使用类型的默认值,例如int默认值为0),但是函数体最后必须有return语句

func getSomeStrings() (a, b string){
    //为命名地返回值赋值
    a = "Part1"
    b = "Part2"
    //必须使用return语句让函数返回
    return
}

//调用如下:
var s1, s2 = getSomeStrings()

5.可变参数的个数

可变参数的个数只能出现在参数列表的末尾

func test1(a uint8, b …string){

​ fmt.Printf(“参数a: %d\n”,a)

​ fmt.Printf(“参数b: %d\n”,b)

}

上述函数中,参数b为可变参数,其个数可以是0个或者多个.

test(16)

test(24, “abcd”)

test(3, “Jack”, “Rose”, “Lucifer”)

下面代码中,test2函数中可变个数参数的定义是错误的,因为他不是参数列表的末尾

func test2(p1 string, p2 …bool, p3 float32){ (✕)

}

可变参数的类型为"切片"(slice), 他是以数组为存储基础的集合类型.在函数体内部,可以使用len函数来获取可变参数的个数,也可以使用

for range 语句来循环访问每一个元素.例如:

func test3(args ...float32){
    n := len(args)
    fmt.Printf("\n\n可变参数的个数: %d\n",n)
    //打印元素
    if n>0 {
        fmt.Println("参数内容:")
        for _,val := range args{
            fmt.Printf("%f",val)
        }
    }
}

6.匿名函数

匿名函数,即没有名称的函数.通常,运用以下两种方法能保证匿名函数可以被访问

  1. 定义一个变量,并且此变量应用匿名函数,当需要匿名函数时,可以通过访问变量来访问.
  2. 定义完立刻调用.这种方法使得匿名函数只能调用一次
var myfun = func (x, y int) int{
    return x*x + y*y
}
//因为函数没有名称,所以在调用时需要通过变量myfun来访问
res : = myfun(2, 4) 

再看一个例子,定义立即调用

func (who string){
    fmt.Prinf("Hello, %s\n",who)
}("Jack")
package main

import "fmt"

func main() {

	//创建新的通道对象
	var ch = make(chan byte)

	go func() {
		fmt.Println("新协程")
		//向通道对象发送数据
		ch<-1
	}()

	//从通道对象接收数据
	<-ch
	fmt.Println("主协程")
}

​ 执行一个新的协程,方法就是在函数调用代码前加上go关键字.加上通道对象(channel)来解决此问题.问题是向通道对象发送数据,数据在主协程中接收

​ 在启动新协程后,主协程代码执行到<-ch这一行,此时通道对象中没有数据,代码会一直处于等待状态.

7.将函数作为参数传递

流程控制

1.顺序执行

2.if语句

if <条件> {<代码块>}

var k string = "check"
if strings.Contains(k,"ch"){
	fmt.Printf("字符串 %s 中包含ch\n", k)
} else {
	fmt.Printf("字符串 %s 中不包含ch\n", k)
}

3.switch语句

3.1基于表达式的switch语句

var mode = 1
	switch mode {
	case 0:
		fmt.Println("关机状态")
	case 1:
		fmt.Println("开机状态")
	case 2:
		fmt.Println("待机状态")
	}

3.2基于类型构建的switch语句

switch语句还可以用变量的类型作为参考表达式,只要某个case语句所指定的类型与表达式所返回的类型相同,该case语句所对应的分支代码就会执行

var x interface{} = "hello"
actvalue := x.(string)
变量 x 声明为interface{}类型(空白接口类型,可兼容任意类型),成为具有动态类型的变量.随后赋给他的实际值string类型,它将动态引用一个string实例. x.(string)表达式完成类型断言,并把变量x引用的值转换成string类型,赋值给actvalue变量.虽然在运行阶段变量x和actvalue的值相同,但他们的数据类型不同
var x interface{} = ......
var actvalue string = .....

而基于类型的switch,要求使用关键字type来代替具体类型,并且作为参考表达式的变量必须声明为接口类型

var c interface{} = 12
	switch v := c.(type) {
	case string:
		fmt.Printf("字符串 : %s\n", v)
	case uint8:
		fmt.Printf("无符号整数 : %d\n", v)
	case int:
		fmt.Printf("有符号整数 : %d\n", v)

	}

对于自定义接口类型,同样可以用switch语句做类型分析

接口的实现原则是必须包含结构体的方法.

3.3fallthrough语句

switch语句在运行阶段只会选择一个case语句执行,即使有多个字句匹配成功,他也只会选择最先匹配的那个分支执行

n := 58
	switch  {
	case n<100:
		fmt.Println("该值小于100")
	case n<80:
		fmt.Println("该值小于80")
	case n<50:
		fmt.Println("该值小于50")
	case n<30:
		fmt.Println("该值小于30")
	}

输出结果: 该值小于100

加上fallthrough

n := 58
	switch  {
	case n<100:
		fmt.Println("该值小于100")
		fallthrough
	case n<80:
		fmt.Println("该值小于80")
		// fallthrough
	case n<50:
		fmt.Println("该值小于50")
	case n<30:
		fmt.Println("该值小于30")
	}

输出结果:

该值小于100
该值小于80

4.for语句

1.仅带条件子句的for

var q = 1
	for q <= 10 {
		fmt.Printf("q 的当前值为: %d\n",q)
		q++
	}

2.带三个子句的for

for [初始化子句] ; [条件子句] ; [更新子句] {
    ......
}
for i := 0; i < 12; i += 2 {
		fmt.Print(i," ")
	}
	fmt.Println()

	var cc = 'a'
	for ; cc <= 122 ; cc++ {
		fmt.Printf("%c", cc)
	}
	fmt.Println()
	//变量cc是rune类型(表示单个字符), 他是int32类型的别名,所以执行cc++运算并不会报错,122是z的ASCII码
	
	for x := 'Z'; x >= 65; {
		fmt.Printf("%c",x)
		x--
	}

0 2 4 6 8 10
abcdefghijklmnopqrstuvwxyz
ZYXWVUTSRQPONMLKJIHGFEDCBA

3.枚举集合元素语句

​ 当for语句带有 range 子句时,它可以通过循环依次从以下对象取出所有值: 字符串(string), 数组(array), 切片(slice), 映射(map)以及(channel)中接收到的值

var str string = "天生我才必有用"
	for i, x := range str{
		fmt.Printf("%2d --------> %c\n",i, x)
	}
	fmt.Print(len(str))

0 --------> 天
3 --------> 生
6 --------> 我
9 --------> 才
12 --------> 必
15 --------> 有
18 --------> 用
21

解释: golang中string底层是通过byte数组实现的。中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8。

var arr = [5]float32{
		1.00085,
		7.001,
		0.0095,
		205.33,
		0.213,
	}
	for index, element := range arr{
		fmt.Printf("[%d]: %f\n", index, element)
	}
	
结果:
[0]: 1.000850
[1]: 7.001000
[2]: 0.009500
[3]: 205.330002
[4]: 0.213000

上面代码首先实例化一个float32数组,然后使用for…range循环语句枚举出所有元素,输出结果包含索引和索引对应的元素.

在使用range字句时,如果只有一个变量接收枚举出来的内容,那么该变量将存储索引值

for index := range arr {
		fmt.Printf("[%d]: %f\n",index, arr[index])
	}

如果不需要索引,for…range循环也可以这样写:

for _,element := range arr{
	...
}

每一轮循环只在element变量中存储元素内容,而索引会被丢弃

映射(map)对象的元素由key和value组成,使用range子句在单次循环中会到两个值,即key和value

var m = map[rune]string{'a':"at", 'b':"bee", 'c':"cut"}
for key, value := range m {
	fmt.Printf("[%c]: %s\n", key, value)
}

在上面的代码,定义的map对象以rune类型key, string类型为value, 包含三个元素.for 循环会得到每个元素的key和value,然后将其输出

range子句也可以枚举通道对象的值,每一轮循环读取一个数值,直到通道对象被关闭.例如:

	//创建通道对象的实例
	var ch = make (chan int)

	//启动新的协程
	go func() {
		//当前代码退出该范围时关闭通道对象
		defer close(ch)

		//向通道发送内容
		ch <- 1
		ch <- 2 
		ch <- 3
		ch <- 4 
		ch <- 5
		ch <- 6
	}()

	//从通道对象中读出所有值
	for  v := range ch {
		fmt.Printf("从通道对象中读出: %d\n", v)
	}

上面代码开启新的协程来向通道对象发送内容,并在主协程通过fo…range循环读出所有的值,调用close函数是关闭通道对象,在通道发送完内容必须显示关闭它,否则,for循环会无限等待新的内容,而通道对象自身也在等待写的内容写入,造成"死锁"现象.加上defer关键字后会使close函数的调用被延迟**(退出当前匿名函数时调用)**

4.contiue与break语句

contiue子句会跳过当前一轮的循环,并从下一轮循环更新的子句开始执行

for a := 10; a > 0; a-- {
		if a == 6 || a == 5 || a == 4 {
			continue
		}
		fmt.Println(a)
	}

10
9
8
7
3
2
1

for a := 10; a > 0; a-- {
		if a == 6 || a == 5 || a == 4 {
			break
		}
		fmt.Println(a)
	}

10
9
8
7

5.代码跳转

在函数内部可以为有特殊用途的代码分配一个标签(label),位于同一函数的其他代码可以使用goto语句进行代码的跳转,并从此处开始执行.

1.代码标签与goto语句
	var str = "uvwxyz"
	if len(str) >= 3 {
		goto L1
	} else {
		goto L2
	}

L1:
	fmt.Println("字符串的长度符合要求")
L2:
	fmt.Println("字符串的长度不足3字节")

字符串的长度符合要求
字符串的长度不足3字节

这样的结果与预期不符,解决方法在L1标签的代码块最后加上return语句

L1:
	fmt.Println("字符串的长度符合要求")
	return
L2:
	fmt.Println("字符串的长度不足3字节")
2.break,continue语句和代码跳转

接口与结构体

1.自定义类型

定义新类型要用到type关键字,和定义变量相似,type关键字可以单行使用,一行定义一个类型,也可以放到一堆小括号内,一次性定义多个类型.

type myType1 ......
type myType2 ......
type myType3 ......
type (
	myType1 ......
	myType2 ......
	myType3 ......
)

可以基于现有的类型来定义新的类型,例如下面的代码基于string类型定义了新的类型name

type name string

name类型与string类型的用法相同,但他们是独立的类型,不妨通过下面的示例来验证

type name string

var a string = "abcde"
var b name = "abcde"
fmt.Printf("变量a的类型: %T\n",a)
fmt.Printf("变量b的类型: %T\n",b)

变量a的类型: string
变量b的类型: main.name

尽管变量a, b引用的内容相同,但由于所属的类型不同,不能进行比较运算.a == b代码会发生错误

当基于现有类型所定义的新类型也无法满足需求时,还可以定义结构体,接口,函数等类型.

//结构体
type car struct{
	id uint
	color uint32
}

//接口
type sender interface {
	writeTo(d string, len int, msg string)
}

//函数
type otherFunc func(x float32) float32

在定义类型时,如果使用了赋值运算符,那表明所定义的类型的仅仅是现有类型的别名,而不是全新的类型.正如下面例子rune 是 int32类型的别名,所以rune类型与int32类型的变量可以进行比较运算

var x rune = 'H'
var y int32 = 72

fmt.Printf("x和y的值相等吗? %t",x == y)  //true

2.结构体

type person struct {
	name string
	age uint8
	weight float32
	height float32
	gender uint8
}

1.结构体的定义

type <结构体名称> struct { <字段名称> }

字段列表的内容可以是可选的,即可以定义没有字段成员的结构体

type atbWorker struct { }

即使字段为空,一对大括号也不能省略

若希望结构体的字段成员能被其他包的代码访问,除了结构体自身的名称大小写需要首字母大写外,其字段成员的名称也要首字母大写.

type Student Struct {
	StdID uint
	Name string
	Age uint8
	email string   //eamil字段只能在当前包中使用
}

2.结构体的实例化

结构体的实例化有多种代码格式,总体可以归纳为两大类----默认初始化和手动初始化

假设有一个fileInfo结构体,用于封装一个数据文件的相关信息

type fileInfo struct {
    name string
    size uint64
    isSysFile bool
    createTime int64
}

    //为字段分配默认值
	var x fileInfo
	//输出字段的值
	fmt.Printf("文件名: %+v\n", x.name)           //  %+v 打印结构体时,会添加字段名
	fmt.Printf("文件大小: %d\n", x.size)		  //  %d      十进制表示
	fmt.Printf("是否为系统文件: %t\n", x.isSysFile)//  %t          true 或 false。
	fmt.Printf("创建时间: %s\n", time.Unix(x.createTime,0))//%s      输出字符串表示(string类型或[]byte)

文件名:
文件大小: 0
是否为系统文件: false
创建时间: 1970-01-01 08:00:00 +0800 CST

也可以这样写

var x = fileInfo{}

x := fileInfo{}

要注意的是:如果声明变量时使用的是指针,那么变量的默认值是nil.此时若直接访问fileInfo就会发生错误,因为指针未引用任何对象,即空指针

var px *fileInfo                          //nil
fmt.Printf("文件名: %s\n", px.name)        //错误

结构体实例化通常需要为字段进行赋值,例如

var y fileInfo
y.name = "dmd.txt"
y.isSysFile = false
y.size = 6955236
y.createTime = time.Date(2022, 1, 22, 14, 57, 0, 0, time.Local).Unix()

这种方法是先定义变量,分配默认值,然后逐个进行赋值.当然,也可以在定义变量后直接赋值

var g = fileInfo{ name:"abc.txt", size:128880, ....}

或者可以将代码分开,多行输入

在多行初始化语句中,最后一个末尾的逗号不能省略

在许多时候,某些字段的默认值正是所需要的值,这种情况就可以忽略部分字段的值

K := fileInfo {
	name: "dex.txt",
	size: 3006265,
	createTime: time.Date(2020, 1 ,1, 23, 15, 4 ,0 ,time.Local).Unix()
}

最后,最简洁的写法,但是要注意一一对应,既不能忽略部分字段

var z = fileInfo{"text.dat",1172362,true,time.Now().Unix()}

如果变量的类型声明为指针类型,那么可以先创建fileInfo实例并完成初始化,然后再用取地址运算符获取地址,再将赋值给指针变量

var c = fileInfo{"text.dll",1172362,true,time.Now().Unix()}
var pc *fileInfo = &c

也可以一步完成

var pc *fileInfo = &fileInfo{"text.dl",1172362,true,time.Now().Unix()}

3.方法

结构体的方法对象并不是在结构体内部定义的,而是在结构体外部以函数的形式定义的.

type test struct {
    
}

func (o test) doSomething() string{
    return "do nothing"
}

方法与一般函数有一点不同,在方法名称前有一个接收参数(上面实例的o参数).该参数传递的是方法所属结构体的实例.

方法调用:

var n test 
s := n.doSomething()

在定义方法时,接受的结构体实例可以是指针类型

func (o *test) doSomething2() string{
	return "do nothind - 2"
}

接收结构体的实例的参数何时使用指针类型,这取决于应用场景.区别如下:

  1. 定义demo结构体,它包含data字段成员
type demo struct {
	data int
}
  1. 为demo结构体定义两个方法.其中setIntV1方法在接受对象参数时只复制demo实例,而setIntV2方法接受的是demo类型的指针,传递的是实例内存的地址
func (x demo) setIntV1(n int){
    x.data = n
}

func (x *demo) setIntV2(n int){
    x.data = n
}
  1. 初始化demo实例
var a = demo{data:100}
  1. 分别调用setIntV1方法和setIntV2方法,并输出调用前后data的字段的值
//情况一: 非指针类型接收demo实例
fmt.Println("---------------传递demo实例的副本--------------------")
fmt.Printf("调用setIntV1方法前,data字段的值: %d\n", a.data)
//调用setIntV1方法
a.setIntV1(200)
fmt.Printf("调用setIntV1方法后,data字段的值: %d\n\n", a.data")
           
//情况二: 以指针类型接收demo实例
fmt.Println("---------------传递demo实例的内存地址--------------------")
fmt.Printf("调用setIntV2方法前,data字段的值: %d\n", a.data)
//调用setIntV2方法
a.setIntV2(200)
fmt.Printf("调用setIntV2方法后,data字段的值: %d\n\n", a.data")

---------------传递demo实例的副本--------------------
调用setIntV1方法前,data字段的值: 100
调用setIntV1方法后,data字段的值: 100

---------------传递demo实例的内存地址------------------
调用setIntV2方法前,data字段的值: 100
调用setIntV2方法后,data字段的值: 200

结论: **当需要在方法内部修改结构体对象的字段时,应该传递该结构体的指针.**如果只是读取结构体字段的值,传递给方法的实例可以是指针类型也可以不用指针类型

3.接口

接口仅包含无实现代码的方法列表.接口能够起到约束和规范类型成员的作用.声明为接口类型的变量可以引用任何与该接口兼容的对象,即被应用的对象类型必须存在与接口类型一致的方法列表.

1.接口的定义

接口只有方法成员,不能包含字段,而且方法中不能包含实现代码.接口类型自身不能实例化,声明变量后默认分配的值是nil

格式:

type <接口名称> interface {
	<方法列表>
}

例如:

type task interface {
	start()
	stop() uint16
	timeout(long int64) bool
}

要注意:在接口声明方法时不需要func关键字,也没有实例对象接收参数,只需要提供方法名称,参数,返回值等特征描述.

方法的命名必须是有效的,而且同一个接口中不能出现重复命名的方法.

type runner interface{
	getContext(key string) (uint64, bool)
	getContext(key int) (uint8, bool)
}

runner接口中,两个getContext方法虽然参数类型与返回值不同,但是他们名称相同,在Go语言中无法通过编译,错误信息如下:

duplicate method getContext

使用空白标识符_作为方法名称是不允许的,因为这样的命名将无法访问.

type musicHub interface {
    play(track uint)(stat int)
    _(title string) int                 //方法名称无效
}
type test interface {
	sendMessage(head, body string) int
}

func main() {
	var ix test
	fmt.Printf("接口类型变量的默认值: %v",ix)

}

接口类型变量的默认值:

2.接口的实现

如果类型T的方法列表与接口T完全一致(方法名称,参数,返回值类型皆相同),那么就可以说类型T实现了接口F.类型T的实例可以赋值给F类型变量.

下面代码中,interLockercustLocker结构体都实现了Locker接口.

type Locker interface {
	Lock() uint16
	Unlock(id uint16)
}

//下面两个结构体均实现了Locker接口
type interLocker struct {
	lockID uint16
}

func (l *interLocker) Lock() uint16 {
	l.lockID++
	fmt.Printf("系统已锁定")
	return l.lockID
}

func (l *interLocker) Unlock(id uint16) {
	if id!=l.lockID {
		fmt.Println("锁定标识不匹配")
		return
	}
	fmt.Println("系统已解锁")
}

type custLocker struct{
	locked bool
}
func (l *custLocker) Lock() uint16{
	fmt.Println("线程已锁定")
	return 0
}
func (l *custLocker) Unlock(id uint16){
	fmt.Println("线程已解锁")
}

你可能感兴趣的:(Go语言程序设计,golang)