hello云胜

技术与生活

0%

go函数的多返回值与错误处理

在go的函数那片文章中,我们已经了解go函数的多返回值是go区别于其他静态编程语言的重要特征。

go语言的错误处理机制也是建立在函数多返回值的基础之上的。

借助多返回值的能力,可以将状态和数据分离,分别放在不同的返回值中。

一般go中的惯用法是,返回值的前几个为数据,最后一个是error类型表示是否出错,如果error是nil,表示没有错误。

error接口

error是go原生内置的接口类型

1
2
3
4
5
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

通常我们使用errors.New或者方便的构造一个error

1
2
err := errors.New("错误原因")
err2 := fmt.Errorf("fmt error")

这种用法很方便,但是错误信息只能是字符串。

如果我们需要更复杂的错误信息,就需要自定义error

比如json包中的一个自定义错误

1
2
3
4
5
6
7
type UnmarshalTypeError struct {
Value string
Type reflect.Type
Offset int64
Struct string
Field string
}

错误处理分支判断

在开发中经常遇到的情况是根据不同的错误原因走不通的分支处理。可能会写出这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data, err := b.someFunc(1)
if err != nil {
switch err.Error() {
case "xxx: xxx原因":
// ... ...
return
case "xx: yy原因":
// ... ...
return
default:
// ... ...
return
}
}

这种写法严重依赖错误的返回信息,产生了代码的强耦合。一旦方法稍微改了下错误的原因,代码就会出错。

从 Go 1.13 版本开始,标准库 errors 包提供了 Is 函数用于判断错误值的类型

1
2
3
4
5
6
7
8
var ErrNotExist = errors.New("the resource not exist")

func TestIs(t *testing.T) {
testErr := fmt.Errorf("notExist,%w", ErrNotExist)
if errors.Is(testErr, ErrNotExist) {
t.Log(testErr.Error())
}
}

将错误作为一个导出错误值。方法调用方使用errors.Is进行判断

或者通过自定义Error的错误类型进行判读

从 Go 1.13 版本开始,标准库 errors 包提供了As函数,判断一个 error 类型变量是否为特定的自定义错误类型

1
2
3
4
5
6
7
8
9
10
11
12
13
type MyError struct {
e string
}
func (e *MyError) Error() string {
return e.e
}
func TestAs(t *testing.T) {
var err = &MyError{"MyError error demo"}
var e *MyError
if errors.As(err, &e) {
println("errinfo:", e.Error())
}
}

errors.As(err, &e)的作用是判断err是否是MyError类型,如果是,赋值给e

变量声明

变量声明的意义:go是静态语言。变量声明是告诉编译器该变量可以操作的内存边界信息,这主要是看变量的类型。

go的变量声明语句:

1
var a int = 10

对比java

1
int a = 10;

go将变量名放在了变量类型的前面,为啥要这样设计?难道只是为了特立独行?

Go’s Declaration Syntax - go.dev

原生类型

零值

如果变量声明时没有显式为变量赋予初始值,Go 编译器会为变量赋予这个类型的零值

go的所有原生类型都有默认值

内置原生类型 默认值/零值
所有整型类型 0
浮点类型 0.0
布尔类型 FALSE
字符串类型 “”
指针、接口、函数、切片、channel、map nil

另外,像数组、结构体这样的复合类型变量的零值就是其组成元素的零值的复合

块声明和单行声明

go可以使用block块进行集中声明

1
2
3
4
5
var {
a int = 100
b int8 = 88
c string = "hello"
}

也可以在一行中进行多个声明

1
var a, b, c int = 4, 5, 6

两者可以组合起来使用

1
2
3
4
var {
a, b, c int = 4, 5, 6
s, t string = "hello", "world"
}

类型推断

go提供了自动类型推断的语法糖,使我们进行变量声明时可以省略类型

1
var a = 10

自动推断的逻辑:根据右侧变量的值推断类型。整型值的默认类型就是int,浮点型的时float64,布尔值的是bool,字符值的是rune,字符串的是string

因为自动推断类型是根据值反推,所有声明语句必须赋值。var a这种声明是不合法的

因为有了类型推断,所以上面的单行语句

1
var a, s, b = 10, "haha", true

短变量声明

类型推断进一步简化,将var关键字省去

1
a := 10

短变量声明也支持单行格式

1
a, s, b := 10, "haha", true

那么问题来了,为什么有两种变量申明方式?

绝不只是为了少敲两次键盘。一个很重要的原因是短变量声明可以很方便的进行重构。

变量作用域

go的变量从作用域上分为两种:包级变量和局部变量。比java简单多了。

如果这个包级变量的首字母大写,那么这个包级变量可以视为一个全局变量。

包级变量

在方法外声明的变量就是包级变量

包级变量只能用var声明,不能用短变量声明

包级变量可以只声明,不进行初始化,go语言也会让这些变量拥有零值。

1
var a int32
代码规范建议
  • 相同类型的变量声明放在同一个var代码块下
  • 显示初始化和非显式初始化的变量声明分开
  • 就近原则。进行在第一次使用这个变量的前面声明变量.当然了,如果一个包级变量在包内部被多处使 用,那么这个变量还是放在源文件头部声明比较适合的。

局部变量

局部变量仅声明而不初始化,只能用var形式。

声明同时进行初始化,建议采用短变量形式

类型系统

数据类型是一门语言最基础的内容。对于静态语言来说,会设计多种不同的数据类型。设计不同数据类型的目的主要是为了在编译阶段根据类型确定分配不同大小的内存。

go和java一样,同属于静态语言阵营。不同于动态语言(python、ruby,javascript等)可以在运行时通过对变量赋值的分析,自动确定内存边界,并且动态语言的变量可以在运行时赋予不同的数据类型。静态语言必须通过变量声明,显式的告诉编译器变量的类型信息。

看起来稍显麻烦,但是go提供了自动分析变量类型的能力。下面你会看到。

java

java中通常将数据类型分为两类:基本数据类型(Primitive Type)和引用数据类型(Reference Type)

基本数据类型

分为8种

类型名称 关键字 占用内存 取值范围
字节型 byte 1 字节 -128~127
短整型 short 2 字节 -32768~32767
整型 int 4 字节 -2147483648~2147483647
长整型 long 8 字节 -9223372036854775808L~9223372036854775807L
单精度浮点型 float 4 字节 +/-3.4E+38F(6~7 个有效位)
双精度浮点型 double 8 字节 +/-1.8E+308 (15 个有效位)
字符型 char 2 字节 ISO 单一字符集
布尔型 boolean 1 字节 true 或 false

引用数据类型

java中没有指针类型。java的引用数据类型就是数组、类和接口

go

go语言的类型,大体上可以分成三类:基本数据类型,复合数据类型和接口类型。

基本类型

  • 一种内置布尔类型:bool
  • 11种内置整数类型:int8uint8int16uint16int32uint32int64uint64intuintuintptr
  • 两种内置浮点数类型:float32float64
  • 两种内置复数类型:complex64complex128
  • 一种内置字符串类型:string

除了这17种基本类型,Go中有两种内置类型别名(type alias):

  • byteuint8的内置别名。 我们可以将byteuint8看作是同一个类型。
  • runeint32的内置别名。 我们可以将runeint32看作是同一个类型。

可以看到,go对整数类型定义的非常细,这样方便我们选择合适的尺寸,编程出最优化内存占用的程序。

平台相关的数据类型

整数类型中intuintuintptr没有写位数,他们的尺寸依赖于具体编译器实现。称为平台相关的数据类型。 也就是说,在64位的架构上,intuint类型的值是64位的;在32位的架构上,它们是32位的。 编译器必须保证uintptr类型的值的尺寸能够存下任意一个内存地址。

所以建议,编写有移植性要求的代码时,最后不要用这种平台相关的数据类型。

浮点型

同样存在精度问题,不可以用于金额相关业务的计算。

否则坑你没商量。

字符串

和java一样,go的string类型是不可变的。

go的优化点:

  • 对多行字符串的支持。

    在java里写多行字符串非常恶心,加一堆换行转义符。go只要用反引号引起来。你写什么就是什么。所见即所得。

  • 采用unicode编码,对中文支持友好。

字符串的长度问题

字符串的长度分字节长度和字符长度。一个汉字是一个字符,在unicode编码方案用utf-8编码存储的情况下,一个汉字是三个字节。

1
2
3
4
5
func TestChinese(t *testing.T) {
var s string = "中国"
t.Log("中国的字节长度是", len(s)) // 6
t.Log("中国的字符长度是", utf8.RuneCountInString(s)) // 2
}
字符

rune这个看起来很陌生,似乎很难理解。但是其实只要类比java的char就好。

rune,在Go中,一个rune值表示一个Unicode码点。 我们可以将一个Unicode码点看作是一个Unicode字符。 但是,我们也应该知道,有些Unicode字符由多个Unicode码点组成。不过,每个英文或中文Unicode字符值含有一个Unicode码点。

可以说,一个 rune 实例 就是一个 Unicode 字符,一个 Go的 字符串可以被视为 rune 实例的集合

一个rune字面量由若干包在一对单引号中的字符组成。包在单引号中的字符序列表示一个Unicode码点值。 rune字面量形式有几个变种,其中最常用的一种变种是将一个rune值对应的Unicode字符直接包在一对单引号中。比如:

1
2
3
'a' // 一个英文字符
'π'
'众' // 一个中文字符
1
2
3
4
5
func Test_rune(t *testing.T) {
var a rune = 'a'
t.Log(a)
t.Log(a == 97) // true
}

打印的结果是97。’a’的unicode编码就是97。两者是等价的。

go中的字符使用unicode编码方案编码的,但是在存储上使用utf-8的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestRune(t *testing.T) {
var c rune = '中'
t.Logf("中的unicode码点%x", c) // 4e2d

buf := make([]byte, 3)
utf8.EncodeRune(buf, c)
t.Logf("中的unicode码点的utf-8存储0x%x", buf) // 0xe4b8ad
// 一个汉字三个字节

// 对utf-8解码
r, size := utf8.DecodeRune(buf)
t.Logf("%s", string(r))
t.Log(size)
}

组合类型

  • 指针类型 - 类似C语言指针

  • 结构体类型 - 类似C语言的结构体

  • 函数类型 - 函数类型在Go中是一种一等公民类别

  • 容器类型

    包括:

    • 数组类型 - 确定长度的容器类型
    • 切片类型 - 动态容量的容器类型
    • map类型- 也常称为字典类型。在标准编译器中映射是使用哈希表实现的。
  • 通道类型 - 通道用来同步并发的协程

  • 接口类型- 接口在反射和多态中发挥着重要角色

类型推断规则

如前所述,go的基本类型,比如int,有很多不同的尺寸类型。所以一个整数100。并不能确定其究竟是什么类型。这种情况在go中就称为值的类型不确定性。(在java中就不存在这种情况)。因为这种不确定性,就需要类型推断的原则。

  • 一个字符串的默认类型是string类型。
  • 一个布尔值的默认类型是bool`类型。
  • 一个整数型的默认类型是int`类型。
  • 一个rune字符的默认类型是rune(亦即int32`)类型。
  • 一个浮点数的默认类型是float64`类型。
  • 一个复数的默认类型是预声明的complex128类型。

类型转换问题

和java一样,go中也可以进行类型转换。这又分两者情况。基本类型和组合类型。

基本类型

在java中

1
2
3
4
5
6
@Test
public void easy() {
int a = 100;
long b = a;
int c = (int)b;
}

范围小的int型可以直接默认转为long型。但是long型转为int必须显式类型转换。

但是在go中

1
2
3
4
5
func Test_Type_Change(t *testing.T) {
var a int8 = 100
var b int64 = int64(a)
var c int16 = int16(b)
}

所有的类型转换,都必须显式进行。个人感觉,go这样处理还是好的,程序员应该知道自己在干什么。java的隐式转换其实除了省的敲两下代码,没什么用。反而容易出故障。

1
2
3
4
5
6
@Test
public void easy() {
float a = 1.3f;
int b = (int) a;
int c = (int) 1.3f;
}
1
2
3
4
5
func Test_Type_Change(t *testing.T) {
var a = float32(1.2) // 合法
var b = int(a) // 合法
var c = int(1.2) // 非法,这样写不允许
}

在go和java中,进行精度的转换都是可以的,但是go中直接进行字面量的类型转换是不允许的,这样做也是没有必须要的。所以不允许是更合理的。

常量

go中声明常量用const,java是final。

1
2
3
4
5
const a int8 = 16
const b = "bbb"
const (
C, D = int16(100), int64(88)
)

很简单。看一下就行,go中这些写法都合法。

但是go中还有一些特殊的写法。

常量声明的自动补全

在声明多个常量时,可以使用省略写法。省略的声明,go编译器在编译代码时会自动寻找前面最近的完整描述进行重写。比如

1
2
3
4
5
6
7
8
const (
X float32 = 3.1416
Y // 这里必须只有一个标识符
Z // 这里必须只有一个标识符

A, B = "Go", "Java"
C, _ // 空标识符是必需有,数量必须对上
)

go编译会自动补全为

1
2
3
4
5
6
7
8
const (
X float32 = 3.1416
Y float32 = 3.1416
Z float32 = 3.1416

A, B = "Go", "Java"
C, _ = "Go", "Java"
)

下划线_ 是go中一个空的占位符。作用是承接多返回值中的一个,但是以后不会使用,只是为了对应上。

iota

这是一个go新手初次看容易懵逼的写法。

1
2
3
4
5
6
7
8
9
10
11
const (
Failed = iota - 1 // == -1 iota = 0
Unknown // == 0 iota = 1
Succeeded // == 1 iota = 2
)

const (
Readable = 1 << iota // == 1 iota = 0
Writable // == 2 iota = 1
Executable // == 4 iota = 2
)

iota是go内置的一个常量,每次const声明时初始值是0。然后使用自动补全语法时,每一次声明,iota加1。

1
2
const a = iota  // iota = 0
const b = iota // iota = 0

所以利用iota和自动补全,可以方便的进行多个常量的初始化。

比如go内置的log包,预定义log打印格式时,就使用iota进行。

1
2
3
4
5
6
7
8
9
10
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23.
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
Lmsgprefix // move the "prefix" from the beginning of the line to before
LstdFlags = Ldate | Ltime // initial values for the standard logger
)

相当于

1
2
3
4
5
6
7
8
9
10
const (
Ldate = 1
Ltime = 2
Lmicroseconds = 4
Llongfile = 8
Lshortfile = 16
LUTC = 32
Lmsgprefix = 64
LstdFlags = 3
)

使用时,如果我希望打印的日志前面加上,日期 时间 文件名。那么就传1 + 2 + 8 = 11

1
log.SetFlags(11)

打印出的日志类似这样

1
2
3
2021/12/02 15:08:17 d:/github/go_learn/src/concurrent/goroutine/main/main.go:11: hello!
2021/12/02 15:08:17 d:/github/go_learn/src/concurrent/goroutine/main/main.go:11: hi!
2021/12/02 15:08:19 d:/github/go_learn/src/concurrent/goroutine/main/main.go:11: hi!

变量声明

go在变量声明的语法上比java灵活很多。

首先,一个最标准的变量声明如下

1
var a int = 10

一个明显的区别:go把变量名放在了变量类型的前面。

为什么?Go’s Declaration Syntax - Go 语言博客 (go-zh.org)

官方有个解释,大体是说,和C相比,在一些复杂函数,参数时指针的情况下,将类型放在后面读起来更容易理解。

自动类型推断

go可以省略类型信息进行变量声明

1
var b = 10

根据前面说的类型推断规则,b会被推断为int型。

除了之前说的类型推断之外,还支持一次声明多个变量

1
var a, b = "Go", false  //可以一次声明多个不同类型的变量

但是

1
var a string, b bool = "Go", false  //语法错误。确定类型的声明,一次多个变量只能是一种类型

还可以使用()一次声明多个

1
2
3
4
var (
a string = "GO"
b bool = false
)

短声明语法

然后,重要的来了。go的短声明语法。

1
a, b := "Go", false

省略了var。

和var声明的一个重要区别是:短声明语法只能用在方法内。

变量的作用域

go和java一样(以及其他一切高级语言),变量都有不同的作用域。

java可以通过private,public等关键字设置变量的作用域。

go没有这些关键字,通过另外一种规范确定作用域。一般而言,可以把go中变量按作用域分成两类:包级变量和局部变量。

image-20211224150846337

可以感觉到go的变量声明很灵活。但是有点重复了。这与go崇尚的简单原则有些冲突。go的作者之一rob pike也曾表示过如果重新来设计一次变量声明语法,大概率会砍掉一些灵活性,保持统一性。

变量遮蔽

go和java一样。没什么好说的。基本上语言都是这样。同名变量,局部变量遮蔽包级变量。等等。

变量遮蔽是我们写代码经常犯的错误,但是很低级。

类型别名

在上面的基本类型中,提到了byteuint8的内置别名,runeint32的内置别名。

类型别名这个说法在java中是不存在的。

go的这个类型别名究竟是怎么回事?

go源码中是这样定义byte和rune的

1
2
3
4
5
6
7
8
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

byte和unit8,rune和int32是完全一样的。

类型别名和类型定义语法上的区别就是多了个=

1
2
3
4
5
var a rune 
var b int32 = 132
a = b //可以直接赋值
var c int = 132
// t.Log(a == c) 类型不匹配,不可以直接比较

类型定义

这里要注意类型别名和类型定义的语法。关键字type用来定义类型

1
2
3
4
5
type Age int
type (
Name string
Sex int8
)

这是类型定义的语法。定义一种新的类型,没有=

  • 类型定义的新类型和原类型是两种不同的类型。虽然他们的底层类型都是int
  • 两个底层类型相同的新类型,相互之间转换需要显式执行。而类型别名的数据可直接转换。
  • 类型定义可以写在func里,作用域也只在这个func里

占位符在打印日志时用处很大。

1、普通占位符

1
2
3
4
5
6
占位符                  说明                      举例                             输出
%v 打印一个内置值的值。 Printf("%v", people) {zhangsan}
%+v 打印结构体时,会添加字段名 Printf("%+v", people) {Name:zhangsan}
%#v 相应值的Go语法表示 Printf("#v", people) main.Human{Name:"zhangsan"}
%T 打印内置的或者自定义的类型 Printf("%T", people) main.Human
%% 字面上的百分号,并非值的占位符 Printf("%%") %

2、布尔占位符

1
2
占位符       说明                举例                         输出
%t truefalse Printf("%t", true) true

3、整数占位符

1
2
3
4
5
6
7
8
9
占位符             说明                                           举例                     输出
%b 二进制表示 Printf("%b", 5) 101
%c 相应Unicode码点所表示的字符 Printf("%c", 0x4E2D) 中
%d 十进制表示 Printf("%d", 0x12) 18
%o 八进制表示 Printf("%d", 10) 12
%q 单引号围绕的字符字面值,由Go语法安全地转义 Printf("%q", 0x4E2D) '中'
%x 十六进制表示,字母形式为小写 a-f Printf("%x", 13) d
%X 十六进制表示,字母形式为大写 A-F Printf("%x", 13) D
%U Unicode格式:U+1234,等同于 "U+%04X" Printf("%U", 0x4E2D) U+4E2D

4、字符串与字节切片

1
2
3
4
5
占位符     说明                                  举例                               输出
%s 输出字符串表示(string类型或[]byte) Printf("%s", []byte("Go语言")) Go语言
%q 双引号围绕的字符串,由Go语法安全地转义 Printf("%q", "Go语言") "Go语言"
%x 十六进制,小写字母,每字节两个字符 Printf("%x", "golang") 676f6c616e67
%X 十六进制,大写字母,每字节两个字符 Printf("%X", "golang") 676F6C616E67

5、浮点数和复数的组成部分(实部和虚部)

1
2
3
4
5
6
7
8
占位符     说明                                                           举例                  输出
%b 无小数部分的,指数为二的幂的科学计数法,
与 strconv.FormatFloat 的 'b' 转换格式一致。例如 -123456p-78
%e 科学计数法,例如 -1234.456e+78 Printf("%e", 10.2) 1.020000e+01
%E 科学计数法,例如 -1234.456E+78 Printf("%e", 10.2) 1.020000E+01
%f 有小数点而无指数,例如 123.456 Printf("%f", 10.2) 10.200000
%g 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%g", 10.20) 10.2
%G 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%G", 10.20+2i) (10.2+2i)

6、指针

1
2
占位符         说明                      举例                             输出
%p 十六进制表示,前缀 0x Printf("%p", &people) 0x4f57f0

Go中的方法

Go 语言从设计之初,就不支持经典的面向对象语法元素,比如类、对象、继 承,等等,但 Go 语言仍保留了名为“方法(method)”的语法元素。当然,Go 语言中 的方法和面向对象中的方法并不是一样的

方法的定义

go中方法的定义和函数的定义很像

1
2
3
4
5
6
7
8
type People struct {
name string
age int
}

func (p People) String() string {
return fmt.Sprintf("people : %s, %s", p.name, strconv.Itoa(p.age))
}
1
2
3
4
func (t *T或T) MethodName(参数列表) (返回值列表) {
// 方法体
}

和函数最大的不同就是这个receiver部分。这个receiver部分是一个类型。作用是指明这个方法的归属。就可以说这个类型有这样一个方法。

每个方法只能有一个 receiver 参数

receiver 参数的基类型本身不能为指针类型或接口类型

Go 要求,方法声明要与 receiver 参数的基 类型声明放在同一个包内。也就是说我们不能给go的原生类型(比如int,string)等添加方法,也不能给其他包的类型添加方法。

方法的本质是函数

java的方法在调用时,实际上编译器会自动将this作为第一参数传入方法中。go的原理也类似。

所以上面的

1
2
3
func (p People) String() string {
return fmt.Sprintf("people : %s, %s", p.name, strconv.Itoa(p.age))
}

可以转换为函数

1
2
3
func String(p People) string {
return fmt.Sprintf("people : %s, %s", p.name, strconv.Itoa(p.age))
}

这种等价转换后的函数的类型就是方法的类型

所以,Go 语言中的方法的本质就是,一个以方法的 receiver 参数 作为第一个参数的普通函数。

receiver的类型选择问题

首先看这个例子

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

type Employee struct {
name string
company string
}

// 值方法
// 值方法的接收者是该方法所属的那个类型值的一个副本。我们在该方法内对该副本的修改不会体现在原值上
func (e Employee) SetName(name string) {
e.name = name
}

// 指针方法
func (e *Employee) setCompany(company string) {
e.company = company
}

func main() {
e := Employee{
name: "alex",
company: "google",
}
fmt.Printf("=======:%s\n", e)

e.SetName("zhangsan")
fmt.Printf("=======:%s\n", e)

e.setCompany("baidu")
fmt.Printf("=======:%s\n", e)

}

结果

1
2
3
=======:{alex google}
=======:{alex google}
=======:{alex baidu}

两个set方法,一个的receiver是类型,一个是类型指针。

之前就说过Go 函数的参数采用的是值拷贝传递,SetName中对receiver的修改,只会影响e的副本,不会影响e本身。

类型是*Employee的setCompany方法,因为传递的是地址,所以会影响。

所以,选择receiver类型的原则:

  1. 如果需要将修改反应在原实例上,用*T

    无论是 T 类型实例,还是 *T 类型实例,都既 可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法。这是go的语法糖自动转换

  2. 还是因为值拷贝问题,用T作为receiver类型,如果实例很大,会有较大的性能开销,这种情况下还是用*T比较好

  3. 根据T类型是否需要实现某个接口。是,则需要使用T类型作为receiver。

    另外Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方 法,以及所有以 T 为 receiver 参数类型的方法。

Go编程规范

编程风格

Go编程规范追求的是编写可读性高的Go代码。以下是5条原则。(按重要性排序)

  • 清晰。一段代码的功能应该是清晰明确的
  • 简单。代码应该尽可能用最简单的方式实现其功能。
  • 简洁。代码的信噪比要高
  • 可维护性。代码应该容易维护。
  • 一致性。代码风格保持一致。

Clarity清晰

可读性的首要目标是要让读代码的人更清晰明确的意识到代码的功能和实现。

提高可读性的手段:有意义的命名,有用的注释,以及高效的代码组织结构

代码是否清晰是根据读者的视角来看的,而不是作者的视角。所以说代码易读比易写更重要。

代码的清晰涉及以下两个问题

这段代码是做什么的?

为了让以后的读者(更大的概率是以后的你自己及)能更好的理解代码的功能,有以下建议

  • 变量的命名应该具有意义
  • 增加注释
  • 代码可以用空行或注释隔开
  • 重构代码,抽象成独立的函数或方法,使代码更加模块化
为什么要写这段代码

代码的根本目的应该能从变量、函数或者包的命名中体现出来。

如果不能通过命名直接说明代码的功能,那么一定要加上注释说明。

尤其是在一些细微差别的场景下,一定写清楚为什么写这段代码

命名规范

go开发中有一些规则是默认的。俗称约定大于配置。及早了解这些规则,可以在入门阶段避免踩坑。

命名

源文件命名

go的源码文件,总是用全小写字母形式的单词组合进行命名。不出现大写单词。也就是说不使用驼峰命名法。(我现在也没想明白为啥不用首字母小写+驼峰命名这种方式)

有人说多个单词直接写在一起,不能用其他分隔符,如下划线分开。

比如,helloworld.go,不能写成hello_world.go。(虽然你这样写了也不会报错,实际上我也看到不少源码是这样写的)

我们要知道下划线在go源文件命名中是有特殊作用的,比如在测试代码源文件的命名都是xxx_test.go。

包名

包名通常使用单个的小写单词

整个 Go 程序中仅允许存在一个名为 main 的包。

一个文件夹下只能有一个package,即一个文件夹下的代码使用同一个包名。

Go语言中的常量

今天记录点的轻松的内容,常量。常量在在一门语言中算是比较简单的内容。但是go语言在常量设计上有一些创新,还挺有趣的。

  • 支持无类型常量
  • 支持隐式类型转换
  • 可用于实现枚举

Go语言引入const关键字来声明变量。

不像java,没有常量的关键字。但是java有enum定义枚举,go没有枚举类型的关键字,通过常量来实现。

无类型常量

首先定义常量和用var声明变量的语法是一样的。

可以显式指定类型,如

1
2
3
4
5
6
const a int = 100
const b int = a + 10

func main() {
fmt.Println(a + b)
}

要注意,go是强类型语言。即使底层类型一致,不同类型也不可以直接进行运算。必须进行显式的类型转换。如:

1
2
3
4
5
type myInt int

const a myInt = 100
const b int = a + 10 // 编译报错:cannot use a + 10 (constant 110 of type myInt) as int value in constant
const b int = int(a) + 10 // ok

但是如果定义常量的时候不指定类型,就是无类型常量

1
2
3
4
5
type myInt int
const a myInt = 100
// 无类型常量,可以编译
const n = 7
const b myInt = a + n

这时候用到无类型常量的另一个特性:隐式类型转换

隐式类型转换

无类型常量也不是说就真的没有类型,它也有自己的默认类型,不过它的默认类型 是根据它的初值形式来决定的。像上面代码中的常量 n 的初值为整数形式,所以它的默认 类型为 int。

这样还是有问题的,int和myInt类型的数据不可以直接进行运算。

所以对于无类型常量参与的表达式求值,Go 编译器会根据上下文中的类型 信息,把无类型常量自动转换为相应的类型后,再参与求值计算。也就是会把n转换为myInt类型。

实现枚举

Go没有原生提供枚举类型。枚举类型 本质上就是一个由有限数量常量所构成的集合,所以用const实现枚举并没有什么问题。

1
2
3
4
5
6
// 枚举
const (
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
)

和常量定义并没有什么区别。

通常这里应该介绍下iota了,但说实话,我并不喜欢iota。

iota称为行偏移量指 示器。

go的枚举有一个特性是自动重复上一行,比如

1
2
3
4
5
const (
MONDAY = 1
TUESDAY
WEDNESDAY
)

那么TUESDAY和WEDNESDAY的值就都是1

这种情况下可以使用iota

1
2
3
4
5
const (
MONDAY = 1 + iota
TUESDAY
WEDNESDAY
)

在每一个const块中,iota的初始值是0,并且每一行代码的iota的值依次递增。所以上述代码等价于

1
2
3
4
5
const (
MONDAY = 1 + 0
TUESDAY = 1 + 1
WEDNESDAY = 1 + 2
)

如果想跳过某个iota值,可以使用_

1
2
3
4
5
6
const (
MONDAY = 1 + iota
- // iota == 1被跳过了
TUESDAY // 3
WEDNESDAY // 4
)

再注意一点,iota是每一行的值依次递增,也就是说于同一行的 iota 即便出现多次,多个 iota 的值也是一样的

1
2
3
4
5
6
const (
MONDAY = 1 + iota + iota + iota // 这三个iota都是0
- // iota == 1被跳过了
TUESDAY // 3
WEDNESDAY // 4
)

iota看起来挺灵活的,并且在go标准库或者很多知名的开源项目中都有使用。但是在我们平时的开发中还是习惯将常量明确的显式的定义出来。因为常量可读性我认为是更加重要的。我可不想看到这个常量定义,再去算算这个常量到底是多少。

还要注意,在已确定的常量定义中插入一行,会导致下面所有常量的值改变。

比如:

1
2
3
4
5
6
const (
SUNDAY = 1 + iota
MONDAY
TUESDAY
WEDNESDAY
)

和java的enum相比,表现力还是差了很多。

Go VS Java

杀手级应用

java : 企业开发领域。spring

go:云原生领域。Kubernetes

相同点

都是静态语言。但是go却有类似动态语言的灵活性,比如自动的类型推断

go语言很适合重构,因为有自动的类型推断,类型重命名的功能

Go作为C语言家族的一员,和C,C++一样有节省内存、程序启动快和代码执行速度快的特点。

(但是相比其他C家族语言,go还有编译快,语法灵活,内置并发支持的优势)

跨平台性,java拥有标榜的跨平台性,通过jvm屏蔽了底层运行平台的差异。

go支持跨平台编译,比如可以在linux平台上编译出windows平台的程序,反之亦然。

关键字

GO

go的25个关键字:

break default func interface select

case defer go map struct

chan else goto package switch

const fallthrough if range type

continue for import return var

java有53个关键字,go在标榜自己语法简单时,也经常拿这个说事。

但是,仔细看下会发现,java所说的53个关键字包括了11种基本类型和2个保留字。而go的25个关键字却是剔除了基本类型。所有具体来对比应该是40Vs25。

占位符

基本类型

专门写

数组

go中的数组分为数组(array)类型和切片(slice)类型。它们最重要的不同是:数组类型的的长度是固定的,而切片类型的长度是可变长的。

数组的长度在声明它的时候就必须给定,并且在之后不会再改变。可以说,数组的长度是其类型 的一部分。

其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定 会包含一个数组。

Go 语言的切片类型属于引用类型,而 Go 语言的数组类型则属于值类型

Go 语言里不存在像 Java 等编程语言中那种令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传 值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。

切片的扩容?

一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而 是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。 在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下 简称原容量)的 2 倍。 但是,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25 倍作为新容量的基准(以下新容量基准)。 新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和 (以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。 另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就 会以新长度为基准。 注意,与前面那种情况一样,最终的新容量在很多时候都要比新容量基准更大一些。更多细节可 参见runtime包中 slice.go 文件里的growslice及相关函数的具体实现。

List

Go 语言的链表实现在其标准库的 container/list代码包

go的list没有java的List的get(int i)方法,不能直接获取第i个元素。着实不好用。

1
2
3
4
5
6
7
8
9
l := list.New()
l.PushBack("aa")
l.PushBack(100)

for i := l.Front(); i != nil; i = i.Next() {
t.Log(i.Value)
// 类型进行了包装
t.Log(reflect.TypeOf(i).Kind())
}

container/ring包中的Ring类型实现的是一个循环链表,循环链表一旦被创建,其长度是不可 变的。

Map

go 映射过程的第一步就是把键值转换为哈希值。在 Go 语言的字典中,每一个键 值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈 希值。

Go 语言规范规定,在键类型的值之间必须可以施加操作符==和!=。换句话说,键类型的值必 须要支持判等操作。由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键 类型不能是这些类型。

别名类型与潜在类型

go中的概念,java中没有对应概念。

go中有一种别名类型

1
type MyString = string

这条声明语句表示,MyString是string类型的别名类型,别名类型与其源类型的 区别恐怕只是在名称上,它们是完全相同的。

别名类型主要是为了代码重构而存在的。

Go 语言内建的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32 的别名类型。

一定要注意,如果我这样声明:

1
type MyString2 string // 注意,这里没有等号。

MyString2和string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其 他任何类型。

这种方式也可以被叫做对类型的再定义。这是把string类型再定义成了另外一个类型 MyString2。

对于这里的类型再定义来说,string可以被称为MyString2的潜在类型。

潜在类型的含义是 某个类型在本质上是哪个类型或者是哪个类型的集合。

潜在类型相同的不同类型的值之间是可以进行类型转换的。

因此,MyString2类型的值与 string类型的值可以使用类型转换表达式进行互转。

但对于集合类的类型[]MyString2与[]string来说这样做却是不合法的,因为 []MyString2与[]string的潜在类型不同,分别是MyString2和string。

另外,即使两个类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也 不能赋值。

作用域

go

在同一个代码块中,可以进行变量的重声明

1
2
3
var oldParam string
// 短变量声明对已有变量进行重声明,但是要求必须有个新变量
newParam, oldParam := "newParam", "oldParam"

变量的重声明要求变量的类型必须时一样的。

注意和不同代码块中的重名变量区分。

类型判断

go

类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的那个值。这 个值当下的类型必须是接口类型

1
value, ok := interface{}(x).([]string)

interface{}(x)将x转为接口类型,在 Go 语言中,interface{}代表空接口,任何类型都是它的实现类型

{}一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含 任何内容的数据结构(或者说数据类型)。

类型转换

T(x)

import

java

go

go中import的包,在使用其方法时,需要带上限定名

1
import "fmt"

使用时需要

1
fmt.Println("hello")

但是,如果我们把导入语句写成

1
import . "fmt"

使用的时候直接

1
Println("hello")

在这个特殊情况下,引入的包中的代码被当前源码文件中的代码,视为当前 代码包中的程序实体。go在查找当前源码文件后会先去查 用这种方式导入的那些代码包。不建议使用.引入

实现与继承

方法或函数

java中称为方法,go中称为函数

在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型。

简单来说,这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的 值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。

而更深层次的含义就是:函数值可以由此成为能够被随意传播的独立逻辑组件

“函数是一等的公民”是函数式编程(functional programming)的重要特征。

函数类型

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

import "fmt"

// 定义一个函数类型
type Printer func(content string) (n int, err error)

// 需要方法参数列表和返回值对应上,名字无所谓
func MyPrinter(name string) (num int, err error) {
return fmt.Println("myPrinter:" + name)
}

func main() {
// 声明的是函数类型
var f Printer
// 实际的函数
f = MyPrinter
// 很像java的抽象方法及其实现
f("test")
}

高阶函数

  1. 接受其他的函数作为参数传入; 或者 2. 把其他的函数作为结果返回。

闭包

方法

go中方法和函数是不同的概念,函数则是独立的程序实体。我们可以声明有名字的函数,也可以声明没名字的函数,还可以把它 们当做普通的值传来传去。我们能把具有相同签名的函数抽象成独立的函数类型,以作为一组输 入、输出(或者说一类逻辑组件)的代表。

方法却不同,它需要有名字,不能被当作值来看待,最重要的是,它必须隶属于某一个类型。

方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指 针类型。

Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之 间的组合。

通道

通道(channel)作为 Go 语言最有特色的数据类型,完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。

Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)

这是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言,这也充分体现了 Go 语言最重要 的编程理念。

通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类 型。

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。

  2. 发送操作和接收操作中对元素值的处理都是不可分割的。

  3. 发送操作在完全完成之前会被阻塞。接收操作也是如此。

1
2
3
4
5
6
7
8
func Test_channel(t *testing.T) {
ch1 := make(chan string, 3)
ch1 <- "a"
ch1 <- "b"
ch1 <- "c"
recv := <-ch1
t.Log("收到第一个元素:", recv)
}

make(chan string, 3) string是该通道的类型,也就是了我们可以通 过这个通道传递什么类型的数据。

后面的3表示通道的容量。所谓通道的容量,就是指通道最多可以缓存多少个元素 值。

当容量为0时,或者不设置容量时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,我 们可以称为缓冲通道,也就是带有缓冲的通道。

一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按 照发送的顺序排列的,先被发送通道的元素值一定会先被接收。

在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会 执行对同一个通道的任意个发送或接收操作中的某一个。

对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。

这里要注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是 在接收操作符右边的那个元素值,而是它的副本!!

另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成 正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。

这两步操作是一个原子操作。

阻塞问题

先说针对缓冲通道的情况。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有 元素值被接收走。这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发 送操作。相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。 这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。 因此而等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等

对于非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直 到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。 也就是说,只有收发双方对接上了,数据才会被传递。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲 通道则在用异步的方式传递数据。

单向通道

单向通道最主要的用途就是约束其他代码的行为。

select语句联用

select语句只能与通道联用。由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达 式,比如接收表达式。

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
func Test_channel4(t *testing.T) {
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}

// 随机选择一个发送

index := rand.Intn(3)
intChannels[index] <- index
t.Logf("向channe%d发送", index)

select {
case e := <-intChannels[0]:
{
t.Logf("0接收到:%d", e)
}
case e := <-intChannels[1]:
t.Logf("1接收到:%d", e)
case e := <-intChannels[2]:
t.Logf("2接收到:%d", e)
default:
t.Log("都没选中")
}

像这样加入了default分支,那么无论涉及通道操作的表达式是否有阻塞, select语句都不会被阻塞。default会被选中执行。

如果没有加入默认分支,那么一旦所有的case表达式都没有满足求值条件,那么select 语句就会被阻塞。直到至少有一个case表达式满足条件为止。

select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时 地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这 时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句而并不会对外层的for语句产生作用。

仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。

如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法 在这些分支中选择一个并执行。

并发

Go 语言不但有着独特的并发编程模型,以及用户级线程 goroutine,还拥有强大 的用于调度 goroutine、对接系统级线程的调度器。

这个调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的 三个主要元素,即:G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩 写)。 其中的 M 指代的就是系统级线程。而 P 指的是一种可以承载若干个 G,且能够使这些 G 适时 地与 M 进行对接,并得到真正运行的中介

从宏观上说,G 和 M 由于 P 的存在可以呈现出多对多的关系。当一个正在与某个 M 对接并运 行着的 G,需要因某个事件(比如等待 I/O 或锁的解除)而暂停运行的时候,调度器总会及时 地发现,并把这个 G 与那个 M 分离开,以释放计算资源供那些等待运行的 G 使用。 而当一个 G 需要恢复运行的时候,调度器又会尽快地为它寻找空闲的计算资源(包括 M)并安 排运行。另外,当 M 不够用时,调度器会帮我们向操作系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

正因为调度器帮助我们做了很多事,所以我们的 Go 程序才总是能高效地利用操作系统和计算机 资源。

主 goroutine

与一个进程总会有一个主线程类似,每一个独立的 Go 程序在运行时也总会有一个主 goroutine。这个主 goroutine 会在 Go 程序的运行准备工作完成后被自动地启用,并不需要我 们做任何手动的操作。

主 goroutine 的go函数就是那个作为程序入口的main函数

当程序执行 到一条go语句的时候,Go 语言的运行时系统,会先试图从某个存放空闲的 G 的队列中获取一 个 G(也就是 goroutine),它只有在找不到空闲 G 的情况下才会去创建一个新的 G。

然而,创建 G 的成本也是非常低的。创建一个 G 并不会像新建一个进程或者一个系统级线程那 样,必须通过操作系统的系统调用来完成,在 Go 语言的运行时系统内部就可以完全做到了

在拿到了一个空闲的 G 之后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数(或者 说该函数中的那些代码),然后再把这个 G 追加到某个存放可运行的 G 的队列中。

请记住,只要go语句本身执行完毕,Go 程 序完全不会等待go函数的执行,它会立刻去执行后边的语句。这就是所谓的异步并发地执行。

异常处理

error

1
2
3
type error interface {
Error() string
}

error类型其实是一个接口类型,也是一个 Go 语言的内建类型。在这个接口类型的 声明中只包含了一个方法Error。这个方法不接受任何参数,但是会返回一个string类型的结 果。

panic

Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需 任何参数,并且会返回一个空接口类型的值。

defer

注意, 被延迟执行的是defer函数,而不是defer语句。

defer语句和recover函数调用,才能够恢复一个已经发生的 panic。

如果一个函数中有多条defer语句,那么那几个defer函数调用的执行顺序是怎样 的?

如果只用一句话回答的话,那就是:在同一个函数中,defer函数调用的执行顺序与它们分别 所属的defer语句的出现顺序(更严谨地说,是执行顺序)完全相反。 当一个函数即将结束执行时,其中的写在最下边的defer函数调用会最先执行,其次是写在它 上边、与它的距离最近的那个defer函数调用,以此类推,最上边的defer函数调用会最后一 个执行。

垃圾回收

main函数对比

习惯上,我们学习一门语言都是从hello_world开始。

先使用一个简单的程序,对比下java和go的代码风格,先有个感性认识。

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.yunsheng;

import java.util.Date;

public class Demo {
public static void main(String[] args) {
int a = 1;
int b = 2;
Demo demo = new Demo();
int c = demo.sum(a, b);
Date now = new Date();
System.out.println(now.toString());
System.out.println(c);
}

private int sum(int a, int b) {
return a + b;
}
}

go

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

import (
"fmt"
"time"
)

func main() {
var a, b = 1, 2
result, err := sum(a, b)
if err == nil {
fmt.Println(result)
}
fmt.Println(time.Now())
}

func sum(a int, b int) (int, error) {
c := a + b
return c, nil
}

我们从第一行开始对比

包的概念

java和go都有package这个关键字。即定义包。

在java代码中,写了package com.yunsheng;那么我的代码一定是在目录com/yunsheng下,包名和目录名必须对应。

而在go中,包名和目录名并不要求对应上,当然强烈建议包名和目录名一致。但是同一个目录下的go源代码文件,必须都是相同的package。

image-20220902093338378

看这个源码,包名,目录名,文件名都可以不一样。

此处包名写为main,是因为go的main函数必须运行在名称为main的包里。

main函数是一个go应用的入口,通常我们将main代码包称为程序代码包或命令代码包。(在工程上,通常将main函数放在名为cmd或app的目录下)。将其他的源码包称为库代码包。

特殊规则

有一个特殊规则:一个引入路径中包含有internal目录名的代码包被视为一个特殊的代码包。 它只能被此internal目录的直接父目录(和此父目录的子目录)中的代码包所引入。比如,代码包.../a/b/c/internal/d/e/f.../a/b/c/internal只能被引入路径含有.../a/b/c前缀的代码包引入。

引入import

这也是两者都有的关键字。

区别在于,java语法简单直接,引入之后,直接使用。没有什么花活。

go就要多说点了

go引入之后,在使用时需要用引入的包名来使用相应的方法,比如

1
fmt.Println(time.Now())

另外,go在imort时可以给包起别名,如

1
import myfmt "fmt"

然后在使用时,就可以用别名

1
myfmt.Println(time.Now())

这是一个非常实用的功能。因为我们经常会遇到引入的包方法同名的情况。在java里,我们需要在调用方法时使用全量包名的方式来解决。go显然提供了更优雅的解决方式。

其实这种方式才是go引入包的标准写法。

1
import "fmt"

是省略了自定义包名,默认使用引入的代码包的包名。

句点引入

一种特殊的import方式

1
2
3
4
import (
. "fmt"
. "time"
)

这种方式引入的,在使用时可以省略包名。就好像是自己写的代码一样。

1
Println(Now())

这种方式不推荐使用,因为会使代码的可读性变差。

匿名引入

1
import _ "fmt"

这种方式是为了加载一下包,使包中的初始化代码得到执行,进行资源初始化。

除了这种匿名引入,其他引入的包必须被使用。如果只引入而不使用,代码编译报错。这点也和java不同

class类

java的核心理念就是面向对象,所以我们的代码一般情况下都是在一个class里

go中没有明确的面向对象的说法。

go的一等公民是函数。你可以发现,go是通过包的引入来使用函数和变量等资源。

如果非要扯上的话,可以将struct比作java中的class。关于struct以后再说。

main函数

go和java的程序入口都是这个名为main的函数。

从定义上来看,显然go的语法要简洁的多。

go定义的函数没有java的public、protected、private等访问控制修饰符。go通过一种有趣的方式声明属性或方法的访问权限:通过首字母的大小写。大写的就是其他包可访问,小写的就只能包内访问。所以你可以看到fmt调用的Println首字母就是大写的。

go中没有static关键字,也不支持相关的静态概念。

变量声明

两者虽然都是静态语言,但是go却觉有不少动态语言的灵活性。

go声明变量可以省略类型

比如第6行的

1
var a, b = 1, 2

省略了变量的类型,go会根据字面量值是数字,自动推断a,b的类型是int

写全应该是

1
var a, b int = 1, 2

另外,go中声明变量还有一种更简单的方式,称为短声明

1
a, b := 1, 2

关于短变量声明和var声明的一个区别是:

在方法内两者都可以。但是在方法外只能用var声明。

方法返回

go支持多返回值。相比较java而言,这是一个让人写代码很舒服的特性。
go中方法的返回值一般设计为(data,error)这种形式。
如果方法执行异常,通过error返回错误。
如果方法成功执行,那么使用data的数据。
所以,方法的调用者可以通过error来判断方法是否正常执行完成。