作为Java程序员,本章内容就走马观花的了解一下即可,这一章内容和其他语言没有太大的不同。

本章学习的内容

  • 包申明、导入、main方法
  • 变量声明(var:=的区别)
  • 基本类型:int、float、string、bool
  • 常量与iota
  • 类型转换

main方法

通过一个HelloWorld的程序,说明第一小节内容:包申明、导入包、main方法。创建一个main.go

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Print("Hello World")
}

包申明

1
package main

在Go语言中,每一个源文件都必须以package开头,它的作用就是定义了该文件属于哪个命名空间。这个名称可以随意定义,但main这个名称很特殊:

  1. 如果该文件的packagemain,那么这个程序将会被编译成一个可执行程序
  2. 对于不同的文件,也可以将包名定义为main,但是main方法只能有一个。

例如在main.go的同一级目录,创建一个utils.go,并且也声明为main

1
2
3
4
5
6
7
8
package main

func max(a, b int) int {
if a > b {
return a
}
return b
}

这样是可以的,但如果在这个文件里也定义一个main方法,作为项目整体运行的时候,就会报错。

错误: 软件包 hellogo 包含多个 main 函数
请考虑改用文件种类

learning-go-01-goland-run-error

如果运行种类那里换成目录,会提示如下错误:

1
2
3
# hellogo
.\utils.go:12:6: main redeclared in this block
.\main.go:9:6: other declaration of main

如果你在终端执行go run utils.go是可以执行的,因为它执行的模式是文件

learning-go-02-go-run-utils

导入包

1
import "fmt"

这就和Java差不多,主要的区别就是不用的包不允许导入。

  • 包名必须使用双引号”“包裹。

  • 导入的包必须使用,否则编译器会报错

  • 如果有多个包,通常使用圆括号

1
2
3
4
import (
"fmt"
"math"
)

main方法

1
2
func main() {
}

Javapublic static void main(String[] args)一样,是程序的入口。

  • func关键字:用于定义方法(函数)
  • main方法不接受任何参数,也不返回任何值。
  • 花括号必须跟在 main()后面, 如果使用c#那种换行会报错。

变量申明

Go语言中,通常变量申明有两种方式:第一种是var,另一种是简洁的申明方式:=,二者略有区别。

单个变量申明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

var global = "全局变量"
//globalval :=100 不能作为全局变量

func main() {
// 声明一个变量
var a1 = "张三"
fmt.Print(a1)

var username string
username = "zhangsan"

a2:= "李四" // 只能申明局部变量
fmt.Println(a2)
}

和早期(JDK11以前)的Java不同,Go语言的类型时自己推断出来的,通常不需要显示的申明变量类型。

上述例子,分别体现了三种声明变量的方式:

  • 类型推导:编译器根据等号右边的值自动判断a1的类型是string,一旦确定,那么a1的类型就会固定,不能再修改为其他类型。
  • 标准声明(var + 类型):会和Java一样,有一个默认值的机制:如果你只声明 var a int 而不赋值,Go 会自动将其初始化为 0(字符串是 "",布尔是 false)。
  • 短变量申明:=:偷懒专用,但只能使用在方法(函数)内部,不能用于包级变量,且必须至少有一个变量是新声明的。

批量变量申明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

func main() {
var name1, name2 = "李四", 22
fmt.Println(name1, name2)
// 声明时就赋值
var (
age1 = 18
age2 = 20
nickname = "王五"
)
fmt.Println(age1, age2, nickname)

// 声明后赋值
var (
username11 string
age11 int
sex11 string
)
username11 = "zhangsan"
age11 = 11
sex11 = "男"
fmt.Println(username11, age11, sex11)

// 使用:=申明多个变量
a, b, c := 1, 2, "C"
fmt.Println(a, b, c)
// 打印类型
fmt.Printf("%T %T", a, c)
}

对于批量变量申明,只需要var(多个变量)即可,非常优雅。非常适合归类,对于同一类的变量可以申明到一起,还增加了可读性。

匿名变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var username, age = getUserInfo()
fmt.Println(username, age)
//匿名变量
var username1, _ = getUserInfo()
fmt.Println(username1)
}
// 定义一个方法
func getUserInfo() (string, int) {
return "zhangsan", 18
}

如果某个方法返回的值不需要,为了避免代码报错,那么可以使用下划线_作为匿名变量,这也是比较常用的方式。

和导入包一样,未使用的变量也会报错,不用变量的请及时清理。(谁懂读别人Java代码的苦啊)

基本类型

Go语言的基本类型有:

整数类型

  • 平台无关(需要明确长度的)
    • 有符号:int8int16int32int64
    • 无符号:uint8uint16uint32uint64
    • 特殊别名:
      • byte:等用于uint8,常用于处理二进制原始数据。
      • rune:等同于int32,专用用于表示一个Unicode字符(Java里的char?)
  • 平台相关
    • int/uint:在 32 位系统上占 4 字节,在 64 位系统上占 8 字节( Debian 13,int 默认就是 64 位)。
    • uintptr:无符号整数,大小足以存放一个指针的位模式,多用于底层 unsafe 编程。

在 Go 中,intint64 被视为不同的类型。即使在 64 位系统上两者的长度相同,你也不能直接将 int 变量赋值给 int64,必须显式转换:int64(myInt)

浮点型

  • float32:单精度浮点数(约7位有效数字)
  • float64: 双精度浮点数(约15位有效数字),它是Go语言的类型推导的默认值。

所有语言的通病,也是面试题位数不懂相同的就是0.1 + 0.2的问题,Go也有!对于Java有BigDecimal,对于Go同样有shopspring/decimal

字符串

字符串string(小写),它是不可变类型,一旦创建,字符串内部的字节序列就不允许修改。其次,它的默认编码是UTF-8,本质上上一个只读的字节切片([]byte)。

有几个坑需要注意:

len()返回的是字符串的字节长度,而非字符个数

1
2
3
s := "Hello Go语言"
//打印出的长度位14(中文每个占用3字节)
t.Log(len(s))

当然,非要统计字符个数,请使用unicode/utf8这个包

1
2
3
4
5
6
7
8
9
import (
"testing"
"unicode/utf8" //要导入这个包
)

s := "Hello Go语言"
// 打印长度为10
t.Log(utf8.RuneCountInString(s))

布尔类型

类型名为bool,默认值为false,取值只有两个:true或者false,不允许像c那样,将0当作false

常量与iota

常量

Go语言的常量使用const定义,可以显式声明类型,也可以利用类型推导。和其他语言一样,编译时就确定了值,并且不可改变。

其规则如下:

  • 编译时确定:常量的赋值必须是一个在编译期就能确定的值(如字面量、算术运算、内置函数如 len())。不能把一个函数的返回值赋值给常量。
  • 批量申明
  • 常量展开(跳过赋值):在一组常量中,如果某一行没有写赋值,那么它会自动复用上一行的表达式。

下面通过一个例子来说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//常量
const pi = 3.14
//pi = 11 会报错
fmt.Println(pi)
// 批量声明
const (
A = "a"
B = "b"
)

fmt.Println(A, B)

//常量展开
const (
n1 = 100
n2
n3
n4
)
// 都是100
fmt.Println(n1, n2, n3, n4)

iota(常量计数器)

iota 是 Go 语言的一个特殊常量,它被设计用来简化累加数字的定义。你可以把它理解为 const 块中的行索引。通常用来做枚举错误码权限位物理单位状态机等。

iota 的基本行为

  • iotaconst 关键字出现时将被重置为 0
  • 每新增一行常量声明,iota 就会自动累加 1
1
2
3
4
5
6
7
8
9
10
11
  // 这里还使用了常量展开,学以致用。
const (
Java = iota
Go
Rust
C
CPP
Python
)
// 0,1,2,3,4,5
t.Log(Java, Go, Rust, C, CPP, Python)

如果像跳过某个值,可以使用如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const (
n1, n2 = iota + 1, iota + 2
n3, n4
n5, n6
)
// 1 2 2 3 3 4
t.Log(n1, n2, n3, n4, n5, n6)

const (
// 跳过0
_ = iota
n7
n8
//跳过3
_
n9
)
// 1 2 4
t.Log(n7, n8, n9)

iota 最强大的地方在于它不仅能代表数字,还能参与位运算。这在定义权限、状态位或文件单位时非常高效。

1
2
3
4
5
6
const (
READABLE = 1 << iota // 1 << 0 = 1 (二进制 001)
WRITEABLE // 1 << 1 = 2 (二进制 010)
EXECUTABLE // 1 << 2 = 4 (二进制 100)
)
t.Log(READABLE, WRITEABLE, EXECUTABLE)

还有存储单位:

1
2
3
4
5
6
7
8
const (
_ = iota
KB = 1 << (iota * 10) // 1 << (10*1) = 1024
MB = 1 << (iota * 10) // 1 << (10*2) = 1048576
GB = 1 << (iota * 10) // 1 << (10*2) = 1073741824
TB = 1 << (iota * 10) // 1 << (10*2) = 1099511627776
)
t.Log(KB, MB, GB, TB)

对于整形和浮点型,还有一个类型的问题:

  • 无类型常量: const x = 123,它们具有“高精度”且没有固定类型,可以根据上下文自动转换。
  • 类型化常量:如 const x int32 = 123。它的类型是死板的,只能与 int32 运算。

类型转换

Go 语言的类型转换遵循一个核心原则:显式大于隐式。它不支持像Java那样的括号强转(int) a,并且只有两种类型相互兼容时,才能转换(数值类型、或者对象底层结构相同)。

语法T(v)

Go 的类型转换非常直观,语法类似于函数调用:将变量 v 转换为类型 T

1
2
3
4
5
6
7
8
9
10
11
// 直接用testing了,省得互相干扰
func Test_Convert(t *testing.T) {
var a int = 18
var b float64 = float64(a)
var c uint = uint(b)
t.Log(a, b, c)
t.Logf("%T,%T,%T", a, b, c)
}

main_test.go:80: 18 18 18
main_test.go:81: int,float64,uint

数值转换时,开发者必须亲自处理溢出精度丢失的问题。

从浮点数转为整数时,小数部分会被直接截断,而不是四舍五入。

1
2
3
4
f := 3.9889
i := int(f)
// 3.9889 3
t.Log(f, i)

如果将大范围类型转换为小范围类型时,会发生截断。

1
2
3
4
var d int64 = 257
var e int8 = int8(d)
// 257 1
t.Log(d, e)

还有一个比较常见的场景,那就是字符串与切片的转换:

字符串与字节切片([]byte)

Go 的字符串底层就是字节数组,转换效率很高,但会发生内存拷贝(除非使用 unsafe 包)。

1
2
3
4
5
6
7
func Test_StringCovert(t *testing.T) {
s := "Hello Go语言"
b := []byte(s)
s2 := string(b)
// Hello Go语言 [72 101 108 108 111 32 71 111 232 175 173 232 168 128] Hello Go语言
t.Log(s, b, s2)
}

字符串与字符切片([]rune

如果需要按字符(Unicode)处理中文,必须转为 []rune

1
2
3
4
5
6
7
func Test_StringCovert(t *testing.T) {
s := "Hello Go语言"
b1 := []rune(s)
s3 := string(b1)
// Hello Go语言 [72 101 108 108 111 32 71 111 35821 35328] Hello Go语言
t.Log(s, b1, s3)
}

字符串与数值的转换

从这里开始,之前的语法T(v)就不好使了。需要使用strconv这个包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main
import (
"strconv"
"testing"
"unicode/utf8"
)

func Test_Str2Num(t *testing.T) {
a1 := 100

// int -> string
s1 := strconv.Itoa(a1)
//100: int ,100: string
t.Logf("%d: %T ,%s: %T", a1, a1, s1, s1)
// string -> int
a2, _ := strconv.Atoi(s1)
// 100: string, 100: int
t.Logf("%s: %T, %d: %T", s1, s1, a2, a2)

// 浮点数
f1 := 1.23554434344

// float -> string
// 需要指定精度和格式
/**
第一个参数 float64
第二个参数 格式化类型:
'f' 代表十进制 -ddd.dddd
'b' 代表二进制 -ddd.ddddp±ddd
'e' 代表科学计数法 -d.ddddde±ddd
'E' 代表科学计数法 -d.ddddE±ddd
'g' 代表十进制或科学计数法 -ddd.dddd
'G' 代表十进制或科学计数法 -ddd.ddddE±ddd
第三个参数 精度
第四个参数 精度范围 32 or 64
*/
sf1 := strconv.FormatFloat(f1, 'f', 2, 64)
// 1.235544: float64, 1.24: string
t.Logf("%f: %T, %s: %T", f1, f1, sf1, sf1)

// string -> float
f2, _ := strconv.ParseFloat(sf1, 64)
// 1.24: string, 1.240000: float64
t.Logf("%s: %T, %f: %T", sf1, sf1, f2, f2)

// bool
flag := true

// bool -> string
s2 := strconv.FormatBool(flag)
//true: bool, true: string
t.Logf("%t: %T, %s: %T", flag, flag, s2, s2)

// string -> bool

b1, err := strconv.ParseBool(s2)
if err == nil {
//true: string, true: bool
t.Logf("%s: %T, %t: %T", s2, s2, b1, b1)
}
}

看样子除了string<->int,其他的都是ParseTFormatT,并且使用ParseT的时候,会返回多返回err,学习阶段使用匿名变量接收即可,实际开发中需要判断错误。