热门IT资讯网

Go语言2-基本数据类型和操作符

发表于:2024-11-28 作者:热门IT资讯网编辑
编辑最后更新 2024年11月28日,主要内容:文件名、关键字、标识符Go程序的基本结构常量和变量数据类型和操作符字符串类型文件名、关键字、标识符所有go源码以.go结尾标识符以字母或下划线开头,大小写敏感_是特殊标识符,用来忽略结果保留

主要内容:

  • 文件名、关键字、标识符
  • 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!

0