Go语言2-基本数据类型和操作符
主要内容:
- 文件名、关键字、标识符
- Go程序的基本结构
- 常量和变量
- 数据类型和操作符
- 字符串类型
文件名、关键字、标识符
所有go源码以.go结尾
标识符以字母或下划线开头,大小写敏感
_是特殊标识符,用来忽略结果
保留关键字(25个):
break //退出循环default //选择结构默认项(switch、select)func //定义函数interface //定义接口select //channelcase //选择结构标签chan //定义channelconst //常量continue //跳过本次循环defer //延迟执行内容(收尾工作)go //并发执行map //map类型struct //定义结构体else //选择结构goto //跳转语句package //包switch //选择结构fallthrough //switch里继续检查后面的分支if //选择结构range //从slice、map等结构中取元素type //定义类型for //循环import //导入包return //返回var //定义变量
Go程序的基本结构
下面就是一段最简单的 Hollo World。看一下go程序的基本结构:
package main // 声明包import "fmt" // 导入包func main() { fmt.PringLn("Hello World")}
package: 任何一个代码文件都隶属于一个包
import: 引用其他包
// 导入多个包可以这么写import("fmt")import("os")// 通常习惯这么写import( "fmt" "os")
可执行程序的包名必须是main,并且一个程序只能由一个main函数。main函数是程序的入口函数。
练习
写一个程序,对于给定的一个数字n,求出所有两两相加等于n的组合。把结果在终端打印出来。
package mainimport "fmt"func list(n int){ for i := 0; i <= n; i++ { fmt.Printf("%d+%d=%d\n", i, n-i, n) }}func main(){ list(10)}
包
包中函数的调用:
- 同一个包中的函数,可以直接调用
- 不同包中的函数,通过包名+点+函数名进行调用
包访问控制规则:
- 大写意味着这个函数/变量是可导出的
- 小写意味着这个函数/变量是私有的,包外部不能访问
示例
一个程序包含两个包add和main,其中add包中有2个变量:Name(string)和age(int)。如何在main包中调用Name和age:
// go_dev\day2\get_var_in_add\add\add.gopackage addvar Name string = "Gordon"var age int = 10// go_dev\day2\get_var_in_add\main\main.gopackage mainimport ( "go_dev/day2/get_var_in_add/add" "fmt")func main(){ fmt.Println("Name =", add.Name) fmt.Println("age =", add.age)}
上面的代码是有问题的,运行后会报错:
H:\Go\src\go_dev\day2\get_var_in_add\main>go run main.go# command-line-arguments.\main.go:10:17: cannot refer to unexported name add.age.\main.go:10:17: undefined: add.age
原因是age是小写,表示这个变量是包私有的,在包外部是不能访问的。把两个文件里的age都改成大写Age再试一下:
H:\Go\src\go_dev\day2\get_var_in_add\main>go run main.goName = Gordonage = 10
包的别名
把上面的main函数的修改一下,导入包,包命换成别名,然后用别名来访问包:
package mainimport ( // "go_dev/day2/get_var_in_add/add" a "go_dev/day2/get_var_in_add/add" "fmt")func main(){ fmt.Println("Name =", a.Name) fmt.Println("age =", a.Age)}
init 函数
每个原文件都可以包含一个(也可以是多个)init函数。init函数会自动被go的运行框架调用。调用的时机是在main函数执行之前。
把上面的add函数修改一下,先声明变量,然后再为变量赋值:
// init函数package addvar Name stringvar Age int// 这些这种写法是不对的Name = "Goldie"Age = 18
执行后报错:
H:\Go\src\go_dev\day2\get_var_in_add2\main>go run main.go# go_dev/day2/get_var_in_add2/add..\add\add.go:8:1: syntax error: non-declaration statement outside function body
go是一门编译型语言,非声明语句都需要在函数里。
所以要么在声明变量的时候,就给一个初始值,要么可以在init函数里为变量赋值:
// init函数package addvar Name stringvar Age int// 这些这种写法是不对的// Name = "Goldie"// Age = 18// 把变量初始化写在init函数里func init(){ Name = "Goldie" Age = 18}
只初始化不引用包
Go不允许引用不使用的包。但是有时你引用包只是为了调用init函数去做一些初始化工作。这就需要使用空标识符即下划线:
// go_dev\day2\init_only\test\test.gopackage testimport "fmt"func init(){ fmt.Println("init test.go")}// go_dev\day2\init_only\main\main.gopackage mainimport ( // "../test" // 导入不使用的包会报错 _ "../test" "fmt")func main(){ fmt.Print("This is main")}
如果不用下划线会报错:
H:\Go\src\go_dev\day2\init_only\main>go run main.go# command-line-arguments.\main.go:4:5: imported and not used: "_/H_/Go/src/go_dev/day2/init_only/test"
函数声明和注释
函数声明: func 函数名 (参数列表) (返回值列表) {}
// 无参数无返回值func add () {}// 有参数,一个返回值func add (a int, b int) int {}// 有参数,多个返回值func add (a, int, b int) (int, int) {}
注释有两种,单行注释:// 和多行注释:/* */
//这样可以写单行注释/* 多行注释也一行也是可以的 *//* 像这样就可以注释掉整块的内容*/
常量
常量使用 const 修饰,代表只读,是不能修改的。
const 只能修饰 boolean、number(int相关类型、浮点类型、complex)、string
语法: const identifier [type] = value
,其中type可以省略
const a string = "Hello World"const a = "Hello World"const Pi = 3.1415926const b = 9/3 // 表达式也是可以的
优雅的写法:
const ( a = 0 b = 1 c = 2)// 更加专业的写法const ( a = iota b c)
例子里有个iota。iota是golang语言的常量计数器,只能在常量的表达式中使用(也就是必须在const里面使用)。
简单讲就是,iota在const关键字出现时被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数自增1(从0开始)。所以第一个值就是0,之后的值依次就是1、2。配合表达式还有下划线等等,也可以有很多高级的写法,效果就是让代码定义常量变的更加优雅。以后在定义一组有规律的常量的时候,可以再深入一下,看看如何用iota优雅的实现。
练习
定义两个常量male=1和female=2,获取当前时间的秒数(time.Now().Second()),如果能被female整除,则终端打印FEMALE,否则打印MALE:
package mainimport ( "time" "fmt")const ( _ = iota male female)func main() { now := time.Now() fmt.Println("当前时间:", now) second := now.Second() fmt.Println("秒数:", second) if second % female == 0 { fmt.Println("FEMALE") } else { fmt.Println("MALE") }}
变量
语法: var identifier type
var a int // 默认为0var b string // 默认为空,即""var c bool // 默认为false// 声明的同时进行初始化var d int = 8var e string = "Hello World"
一次定义多个变量,可以写在一个var里:
// 效果和上面一样var ( a int b string c bool d = 8 // 这个有初始值,可以省略类型,go自己会做类型推导 e = "Hello World")
练习:
写一个程序,获取当前运行的操作系统的名称和PATH环境变量的值,打印到终端:
package mainimport ( "fmt" "os")func main() { os := os.Getenv("os") fmt.Println("OS", goos) path := os.Getenv("PATH") fmt.Println("PATH", path)}
值类型和引用类型
值类型:变量直接存储值,内存通常在栈中分配。
基本数据类型int、float、bool、string,以及数组和struct,这些都是值类型。
引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
指针、slice、map、chan等都是引用类型。
上面提到了栈(栈是后进先出的)和堆。如果是值类型,那么数值就直接存在栈里。如果是引用类型,这个变量也是在栈里的,但是栈里的值存的是堆里对应的地址,通过栈里的地址可以查找到堆里对应的地址空间里的值。
下面用int和指针演示一下,a是int值类型,b是指针引用类型。在modify里修改了两个变量的值,在main里打印的结果看,只有b的值被改变了。a的值没变,是因为调用函数传参的时候,是把值类型的值再复制一份给调用的函数的,所以在那个函数里的变化不会影响到原来的变量的值。
package mainimport "fmt"func modify(x int, y *int){ x = 15 *y = 25}func main(){ var( a = 10 b = 20 ) modify(a, &b) fmt.Println(a, b)}
变量的作用域
在函数内部声明的变量叫做局部变量,生命周期仅限于函数内部。
在函数外部声明的变量叫做全局变量,生命周期作用于整个包,如果是大写的,则还可以在外部访问。
数据类型和操作符
bool类型
只能存true或false,默认值是false。
相关操作符:!、&&、||
数字类型
主要有int、int8、int16、int32、int64、uint8、uint16、uint32、uint64、float32、float64。uint开头的这些是无符号×××。
类型转换:type(variable),比如:var a int=8; var b int32=int32(a)
。
逻辑操作符:==、!=、<、<=、>、>=
数学操作符:+、-、*、/ 等等
练习
使用 math/rand 生成10个随机整数,10个小于100的随机整数,10个随机浮点数。
先去官网(https://go-zh.org/ )查一下这个包的用法。右上角有个包点进去找到要查的包。
具体在这里:https://go-zh.org/pkg/math/rand/
// go_dev\day2\random\create\create_num.gopackage createimport ( "math/rand" "fmt")func CreateInt(){ for i := 0; i < 10; i++{ fmt.Print(rand.Int(), ", ") } fmt.Print("\n")}func CreateIntn(){ for i := 0; i < 10; i++{ fmt.Print(rand.Intn(100), ", ") } fmt.Print("\n")}func CreateFloat32(){ for i := 0; i < 10; i++{ fmt.Print(rand.Float32(), ", ") } fmt.Print("\n")}// go_dev\day2\random\main\main.gopackage mainimport "../create"func main(){ create.CreateInt() create.CreateIntn() create.CreateFloat32()}
执行结果:
H:\Go\src\go_dev\day2\random\main>go run main.go5577006791947779410, 8674665223082153551, 6129484611666145821, 4037200794235010051, 3916589616287113937, 6334824724549167320, 605394647632969758, 1443635317331776148, 894385949183117216, 2775422040480279449,94, 11, 62, 89, 28, 74, 11, 45, 37, 6,0.20318687, 0.3608714, 0.5706733, 0.8624914, 0.29311424, 0.29708257, 0.752573, 0.20658267, 0.865335, 0.69671917,H:\Go\src\go_dev\day2\random\main>
执行几次发现,每次结果都是一样的。因为生成随机数要有一个种子,具体要看一下 func Seed(seed int64)
这个方法,默认就是Seed(1),种子一样,每次生成的序列就是一样的。在生成数字之前先设置一下种子,把main改一下:
// go_dev\day2\random\main\main.gopackage mainimport ( "../create" "math/rand" "time")func main(){ rand.Seed(time.Now().Unix()) // 把时间作为种子,每秒生成的随机数是不同的 create.CreateInt() create.CreateIntn() create.CreateFloat32()}
上面的做法不是很专业,设置种子可以认为是一个初始化操作,可以放到init函数里。最后完整的代码如下:
// go_dev\day2\random\create\create_num.gopackage createimport ( "math/rand" "fmt" "time")func init(){ fmt.Println("设置随机数种子...") rand.Seed(time.Now().Unix())}func CreateInt(){ for i := 0; i < 10; i++{ fmt.Print(rand.Int(), ", ") } fmt.Print("\n")}func CreateIntn(){ for i := 0; i < 10; i++{ fmt.Print(rand.Intn(100), ", ") } fmt.Print("\n")}func CreateFloat32(){ for i := 0; i < 10; i++{ fmt.Print(rand.Float32(), ", ") } fmt.Print("\n")}// go_dev\day2\random\main\main.gopackage mainimport "../create"func main(){ create.CreateInt() create.CreateIntn() create.CreateFloat32()}
字符类型
byte 字符类型就是1个字符:var a byte
byte要用单引号表示,单引号只能有一个字符。输出会返回这个字符的ascii码,如果想输出为字符需要用string()函数转换一下:
package mainimport "fmt"func main() { var ( b1 byte b2 byte b3 byte ) b1 = 'a' b2 = 'A' b3 = 98 // 直接用ascii码也可以定义 fmt.Println(b1, string(b1)) fmt.Println(b2, string(b2)) fmt.Println(b3, string(b3)) var b4 int = 99 fmt.Println(b4, string(b4))}
字符串类型
string 字符串类型,是由0个或多个字符组成的:var str string
字符串有2种表示方式:
- 双引号,内部会做转义
- 反引号,内部不会做转义。效果和python里的3个引号似乎一样
示例代码:
package mainimport "fmt"func main() { str1 := "Hello \n How are you" str2 := `Fine \n Thank you` fmt.Println(str1) fmt.Println(str2) str3 := `百日依山尽,黄河入海流。欲穷千里目,更上一层楼。` fmt.Println(str3)}
字符串操作
字符串输出,主要是fmt模块,之前已经用了很多次了。要格式化输出就使用 fmt.Printf()
。
各种格式化详细的写法可以翻一下官方中文文档:https://go-zh.org/pkg/fmt/
fmt.Print()
: 按默认格式输出fmt.Printf()
: 格式化输出fmt.Println()
: 按默认格式输出,且总在最后追加一个换行符。
另外还有3个对应的S开头的命令 Sprint、Sprintf、Sprintln,结果不输出到终端,而是return返回。
字符串格式化:这里的 Sprintf 就能够实现了。如果是格式化输出,就用 Printf 。
把数值转化成字符串
用Sprint()方法可以把屏幕结果输出到变量里去。显示在屏幕上的一定是字符串形式,结果不定向到标准输出,而是定向到变量:
package mainimport "fmt"func main(){ var n int n = 99 s1 := string(n) fmt.Println(s1) // 转类型输出的是对应的ascii字符 fmt.Print(n, "\n") // 直接终端打印,输出的正确的形式 s2 := fmt.Sprint(n) // 把结果返回给变量s2 fmt.Println(s2) // 现在s2里存的是数字的字符串形式}
打印变量类型
%T 的效果是,相应值的类型的Go语法表示
package mainimport "fmt"func main(){ var b5 byte b5 = 'B' fmt.Printf("%T\n", b5) b6 := 'B' fmt.Printf("%T\n", b6) f1 := true fmt.Printf("%T\n", f1) s1 := "abc" fmt.Printf("%T\n", s1)}// 运行结果:H:\Go\src\go_dev\day2\examples>go run show_type.gouint8int32boolstringH:\Go\src\go_dev\day2\examples>
这里的字符类型,并没有当做字符类型来记录,而且两种定义方法所对应的类型也不同。
字符串切片
直接上例子:
package mainimport "fmt"func main(){ str := "This is Golang" fmt.Println(str[0:4], str[:4]) fmt.Println(str[5:]) fmt.Println(str[0:len(str)]) // 缺省的起始位置是0,结束位置是len(str) fmt.Println(str[len(str)-6:len(str)]) // 不支持负数,要截最后几位应该该是这样 fmt.Println(str[5:5+2]) // 要截取多少位,貌似也只能做下加法了 fmt.Printf("%c\n", str[6]) // 只取一个字符,str[6]取到的是字符,就是byte类型 fmt.Println(str[6:6+1]) // 这样可以取到string类型的一个字符,也就是长度为1的字符串}// 执行结果H:\Go\src\go_dev\day2\examples>go run slice.goThis Thisis GolangThis is GolangGolangisssH:\Go\src\go_dev\day2\examples>
字符串拼接
字符串拼接有好几种方法,主要是要考虑性能,各种实现方式结论如下(性能依次提高):
- 性能要求不太高的场合,直接使用运算符,代码更简短清晰,能获得比较好的可读性
- 如果需要拼接的不仅仅是字符串,还有数字之类的其他需求的话,可以考虑 fmt.Sprintf()
- 在已有字符串数组的场合,使用 strings.Join() 能有比较好的性能
- 在一些性能要求较高的场合,尽量使用 buffer.WriteString() 以获得更好的性能
目前只会前2种。
练习
写一个方法,实现字符串的反转(输入"123456",返回"654321"):
// go_dev\day2\reverse\reverse\reverse.gopackage reverseimport "fmt"func Reverse(a string) string { length := len(a) var result string fmt.Printf("%T\n", a[1]) // a[1]的类型是byte,输出:uint8 fmt.Printf("%T\n", a[1:1+1]) // a[1:1+1]是1个字符的字符串,输出:string for i := length-1; i >= 0 ; i-- { //result += string(a[i]) // 用a[i]的话还要转类型 result += a[i:i+1] } return result}// go_dev\day2\reverse\main\main.gopackage mainimport ( "fmt" "../reverse")func main(){ fmt.Println(reverse.Reverse("123456"))}
课后作业
一、判断 101-200 之间有多少个素数(也叫质数),并输出所有素数。
二、打印出100-999中所有的"水仙花数"。
所谓"水仙花数"是指一个三位数,其各位数字立方和等于该数本身。
例如:153 是一个"水仙花数",1**3=3,5**3=125,3**27,sum(1, 125, 27) = 153。
三、对于一个数n,求n的阶乘之和,即: 1! + 2! + 3! + ... + n!