主要内容:
- 文件名、关键字、标识符
- Go程序的基本结构
- 常量和变量
- 数据类型和操作符
- 字符串类型
文件名、关键字、标识符
所有go源码以.go结尾
标识符以字母或下划线开头,大小写敏感
_是特殊标识符,用来忽略结果
保留关键字(25个):
break //退出循环
default //选择结构默认项(switch、select)
func //定义函数
interface //定义接口
select //channel
case //选择结构标签
chan //定义channel
const //常量
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 main
import "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.go
package add
var Name string = "Gordon"
var age int = 10
// go_dev\day2\get_var_in_add\main\main.go
package main
import (
"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.go
Name = Gordon
age = 10
包的别名
把上面的main函数的修改一下,导入包,包命换成别名,然后用别名来访问包:
package main
import (
// "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 add
var Name string
var 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 add
var Name string
var Age int
// 这些这种写法是不对的
// Name = "Goldie"
// Age = 18
// 把变量初始化写在init函数里
func init(){
Name = "Goldie"
Age = 18
}
只初始化不引用包
Go不允许引用不使用的包。但是有时你引用包只是为了调用init函数去做一些初始化工作。这就需要使用空标识符即下划线:
// go_dev\day2\init_only\test\test.go
package test
import "fmt"
func init(){
fmt.Println("init test.go")
}
// go_dev\day2\init_only\main\main.go
package main
import (
// "../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.1415926
const 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 main
import (
"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 // 默认为0
var b string // 默认为空,即""
var c bool // 默认为false
// 声明的同时进行初始化
var d int = 8
var e string = "Hello World"
一次定义多个变量,可以写在一个var里:
// 效果和上面一样
var (
a int
b string
c bool
d = 8 // 这个有初始值,可以省略类型,go自己会做类型推导
e = "Hello World"
)
练习:
写一个程序,获取当前运行的操作系统的名称和PATH环境变量的值,打印到终端:
package main
import (
"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 main
import "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.go
package create
import (
"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.go
package main
import "../create"
func main(){
create.CreateInt()
create.CreateIntn()
create.CreateFloat32()
}
执行结果:
H:\Go\src\go_dev\day2\random\main>go run main.go
5577006791947779410, 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.go
package main
import (
"../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.go
package create
import (
"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.go
package main
import "../create"
func main(){
create.CreateInt()
create.CreateIntn()
create.CreateFloat32()
}
字符类型
byte 字符类型就是1个字符:var a byte
byte要用单引号表示,单引号只能有一个字符。输出会返回这个字符的ascii码,如果想输出为字符需要用string()函数转换一下:
package main
import "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 main
import "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返回。
把数值转化成字符串
用Sprint()方法可以把屏幕结果输出到变量里去。显示在屏幕上的一定是字符串形式,结果不定向到标准输出,而是定向到变量:
package main
import "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 main
import "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.go
uint8
int32
bool
string
H:\Go\src\go_dev\day2\examples>
这里的字符类型,并没有当做字符类型来记录,而且两种定义方法所对应的类型也不同。
字符串切片
直接上例子:
package main
import "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.go
This This
is Golang
This is Golang
Golang
is
s
s
H:\Go\src\go_dev\day2\examples>
字符串拼接
字符串拼接有好几种方法,主要是要考虑性能,各种实现方式结论如下(性能依次提高):
- 性能要求不太高的场合,直接使用运算符,代码更简短清晰,能获得比较好的可读性
- 如果需要拼接的不仅仅是字符串,还有数字之类的其他需求的话,可以考虑 fmt.Sprintf()
- 在已有字符串数组的场合,使用 strings.Join() 能有比较好的性能
- 在一些性能要求较高的场合,尽量使用 buffer.WriteString() 以获得更好的性能
目前只会前2种。
练习
写一个方法,实现字符串的反转(输入“123456”,返回“654321”):
// go_dev\day2\reverse\reverse\reverse.go
package reverse
import "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.go
package main
import (
"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!
有疑问加站长微信联系(非本文作者)