第一个Go语言程序
gopath目录
gopath目录就是我们存储我们所编写源代码的目录.该目录下有三个字目录:src, bin, pkg.
src--->里面每一个子目录,就是一个包.包含Go的源码文件
pkg--->编译后生成的,包含目标文件
bin--->生成可执行文件
GOPATH环境变量
把我们自己存放go语言的目录告诉计算机就可了
新建一个环境变量,然后告诉计算机
步骤
1.新建一个文件加,'goprojects',这个就是存放我们自己开发的程序
2.仿照go官方目录,创建'src(目录,用于存放go语言程序的源码)'
'pkg(目录,用于存放引用其他第三方的程序代码)'
'bin(目录,用于存放编译后的二进制文件(可执行文件))'
main函数和init函数
- init函数是用于程序执行前做包的初始化函数,比如初始化包里的变量等
- 每个包可以有多个init函数
- 包里的源文件也可以多个init函数
- 同一个包中多个init函数的执行顺序go语言没有明确的定义
- 不同包的init函数按照包的包导入的依赖关系决定该初始化函数的执行顺序
- 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() {
}
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秒
- Hours方法
- Minutes方法
- Seconds方法
- Milliseconds方法
- Microseconds方法
- Nanoseconds方法
4Time类型
精度为纳秒
- Now方法: 获取当前系统时间,返回Time实例
var ct = time.Now()
- 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 <函数名称> ([参数列表]) ([返回值列表]){
//内部代码
}
注意点:
- func关键字是必须的,不能省略
- 函数名称一般由字母与数字组成,但不能以数字开头
- 输入参数列表是可选的,如果没有参数,也要保留一对小括号
- 返回值列表是可选的,若无返回值,可以省略
- 函数体写在一对大括号内,与普通代码无异
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.匿名函数
匿名函数,即没有名称的函数.通常,运用以下两种方法能保证匿名函数可以被访问
- 定义一个变量,并且此变量应用匿名函数,当需要匿名函数时,可以通过访问变量来访问.
- 定义完立刻调用.这种方法使得匿名函数只能调用一次
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"
}
接收结构体的实例的参数何时使用指针类型,这取决于应用场景.区别如下:
- 定义demo结构体,它包含data字段成员
type demo struct {
data int
}
- 为demo结构体定义两个方法.其中
setIntV1
方法在接受对象参数时只复制demo
实例,而setIntV2
方法接受的是demo
类型的指针,传递的是实例内存的地址
func (x demo) setIntV1(n int){
x.data = n
}
func (x *demo) setIntV2(n int){
x.data = n
}
- 初始化
demo
实例
var a = demo{data:100}
- 分别调用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类型变量.
下面代码中,interLocker
和custLocker
结构体都实现了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("线程已解锁")
}