Go和Java的异同

一、面向对象

1.基本区别

Java是纯正的面向对象语言,Go相对于Java而言,就比较简洁,没有例如类的继承、接口的实现、构造函数和析构函数、隐藏的 this 指针等,也没有 public、protected、private 之类的访问修饰符。

2.类型系统

类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:

  • 基本类型,如 byte、int、bool、float、string 等;
  • 复合类型,如数组、切片、字典、指针、结构体等;
  • 可以指向任意对象的类型(Any 类型);
  • 值语义和引用语义;
  • 面向对象,即所有具备面向对象特征(比如成员方法)的类型;
  • 接口。
    类型系统描述的是这些内容在一个语言中如何被关联。
    (1)Java中存在两套完全独立的类型系统:值类型系统和以 Object 类型为根的对象类型系统(类)。
    Java 语言中的 Any 类型就是整个对象类型系统的根 —— java.lang.Object 类型,只有对象类型系统中的实例才可以被 Any 类型引用(值类型需要装箱)。
    (2)Go 语言中的大多数类型都是值语义,包括:
  • 基本类型,如布尔类型、整型、浮点型、字符串等;
  • 复合类型,如数组、结构体等(切片、字典、指针和通道都是引用语义);
    所有值语义类型都支持定义成员方法,包括内置基本类型。不过在此之前,需要将基本类型通过 type 关键字设置为新的类型,类似Java的装箱功能,将基本的数字类型转化为面向对象类型。
    (3)Go的接口实现,只需要实现该接口要求的所有方法即可,无需显式声明实现的接口(实际上,Go 语言根本就不支持传统面向对象编程中的继承和实现语法)。
    (4)任何类型都可以被 Any 类型引用。在 Go 语言中,Any 类型就是空接口,即 interface{}。

3.类相关的实现

(1)Go 语言的面向对象编程与Java 完全不同,没有 class、extends、implements 之类的关键字和相应的概念,而是借助结构体来实现类的声明。
(2)Go 语言中也不支持构造函数、析构函数,取而代之地,可以通过定义形如 NewXXX 这样的全局函数(首字母大写)作为类的初始化函数。
(3)在 Go 语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如 bool 类型的零值为 false,int 类型的零值为 0,string 类型的零值为空字符串,float 类型的零值为 0.0。
(4)由于 Go 语言不支持 class 这样的代码块,要为 Go 类定义成员方法,需要在 func 和方法名之间声明方法所属的类型(有的地方将其称之为接收者声明)

func (s Student) GetName() string {
	return s.name
}

(5)在类的成员方法中,可以通过声明的类型变量来访问类的属性和其他方法(Go 语言不支持隐藏的 this 指针,所有的东西都是显式声明)。

func (s *Student) SetName(name string) {
	s.name = name
}

这里的set方法与get不同,需要传入指针,是因为SetXXX 方法需要在函数内部修改成员变量的值,并且该修改要作用到该函数作用域以外,所以需要传入指针类型(结构体是值类型,不是引用类型,所以需要显式传入指针)。
(6)以把接收者类型为指针的成员方法叫做指针方法,把接收者类型为非指针的成员方法叫做值方法,二者的区别在于值方法传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部传入结构体实例的值拷贝,修改不会作用到外部传入的结构体实例。
一个自定义数据类型的方法集合中仅会包含它的所有「值方法」,而该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」,指针方法可以修改所属类型的属性值,而值方法则不能。
(7)Java 支持默认调用类的 toString 方法以字符串格式打印类的实例,Go 语言也有类似的String()方法。

4.Go通过组合实现类的继承和方法的重写

(1)严格来说,Go 语言并不是一门面向对象编程语言,至少不是面向对象编程的最佳选择(Java 才是最根正苗红的),不过我们可以基于它提供的一些特性来模拟实现面向对象编程。要实现面向对象编程,就必须实现面向对象编程的三大特性:封装、继承和多态。
(2)封装:将函数定义为归属某个自定义类型,这就等同于实现了类的成员方法,如果这个自定义类型是基于结构体的,那么结构体的字段可以看做是类的属性。
(3) Go 虽然没有直接提供继承相关的语法实现,但是我们通过组合的方式间接实现类似功能,所谓组合,就是将一个类型嵌入到另一个类型,从而构建新的类型结构。更灵活,没有Java单继承的限制。
还可以通过任意调整被组合类型的位置来改变类的内存布局。
(4)多态:可以通过在子类中定义同名方法来覆盖父类方法的实现,在面向对象编程中这一术语叫做方法重写。只不过 Go 语言不同于 Java,没有专门提供引用父类实例的关键字(super、parent 等)。
这种同一个方法在不同情况下具有不同的表现方式,就是多态。
(5)在 Go 语言中,还可以以指针方式继承某个类型的属性和方法。
当我们通过组合实现类之间的继承时,由于结构体实例本身是值类型,如果传入值字面量的话,实际上传入的是结构体实例的副本,对内存耗费更大,所以组合指针类型性能更好。

5.类属性和成员方法的可见性

(1)Java提供了四个关键字修饰属性和方法的可见性,分别是private(类私有)、默认(包私有)、protected(包和子类私有)和public(公共)。
(2)Go没有类似Java的权限关键字,Go 语言基于包为单位组织和管理源码,因此变量、类属性、函数、成员方法的可见性都是基于包这个维度的。不管是变量、函数,还是自定义类的属性和成员方法,它们的可见性都是根据其首字母的大小写来决定的(大写-包外可访问,小写-仅包内可见)。

6.Go语言的接口和Java的接口概念完全不同。

Java中,接口作为不同类之间强制实现的契约存在。Java是单继承的语言,无论是类与类的继承还是类与接口的实现,都有着严格的层级关系,而且必须显式地声明。这种接口叫做侵入式接口。
Go语言中,一个类只要实现了某个接口要求的所有方法,我们就说这个类实现了该接口(没有显式声明)。这种接口叫做非侵入式接口。
Go中的接口继承与类的继承类似,也是通过组合来完成的。
Go中的接口只包含方法,不包含任何属性。Java中的接口除了抽象方法还有常量。

7.在Go和Java中,接口都不支持直接实例化

不同的是,Go接口支持赋值操作,从而快速实现接口与实现类的映射,与之相比,Java 要实现接口与实现类的映射,只能基于 IoC 容器通过依赖注入实现,要复杂得多。
接口赋值在 Go 语言中分为如下两种情况:

  • 将实现接口的类实例赋值给接口;
  • 将一个接口赋值给另一个接口。
    (1)如果类中实现接口的成员方法都是值方法,则进行接口赋值时,传递类实例的值类型或者指针类型均可,否则只能传递指针类型实例,从代码性能角度来说,值拷贝需要消耗更多的内存空间,统一使用指针类型代码性能会更好。
var a Integer = 1
var m Math = a
fmt.Println(m.Add(1))
var a Integer = 1
var m Math = &a
fmt.Println(m.Add(1))

(2)在 Go 语言中,只要两个接口拥有相同的方法列表(与顺序无关),那么它们就是等同的,可以相互赋值。不过,这里有一个前提,那就是接口变量持有的是基于对应实现类的实例值,所以接口与接口间的赋值是基于类实例与接口间的赋值的。

var num1 Number = 1
var num2 Number1 = num1
var num3 Number2 = num2

此外,接口赋值并不要求两个接口完全等价(方法完全相同)。如果接口 A 的方法列表是接口 B 的方法列表的子集,那么接口 B 也可以赋值给接口 A。

8.类型断言

(1)Java提供了 instanceof 关键字来进行接口和类型的断言,这种断言其实就是判定一个对象是否是某个类(包括父类)或接口的实例。
(2)Go没有提供类似的关键字,而是通过类型断言运算符 .(type) 来实现,其中 type 对应的就是要断言的类型。
注意:在 Go 语言结构体类型断言时,子类的实例并不归属于父类,即使子类和父类属性名和成员方法列表完全一致,因为类与类之间的「继承」是通过组合实现的,并不是 Java中的父子继承关系。父类实现了某个接口,不代表组合类它的子类也实现了这个接口。(其实这里已经和传统的面向对象编程中的父子类完全不是一个概念了,其本质原因就是 Go 使用了组合而非继承来构建类与类之间的关联和层次关系。)
(3)Go还可以基于反射在运行时动态进行类型断言,使用 reflect 包提供的 TypeOf 函数即可实现。
对于基本数据类型,比如 int、string、bool 这些,不必通过反射,直接使用 variable.(type) 表达式即可获取 variable 变量对应的类型值

9.空接口

(1)Java号称血统最纯正的面向对象编程语言中,「万事万物皆对象」,所有类都继承自Object类型,所以 Object 类型变量可以指向任何类的实例。
(2)Go 语言打破了传统面向对象编程中类与类之间继承的概念,而是通过组合实现方法和属性的复用,所以不存在类似的继承关系树,也就没有所谓的祖宗类,而且类与接口之间也不再通过 implements 关键字强制绑定实现关系,所以 Go 语言的面向对象编程非常灵活。
在 Go 语言中,类与接口的实现关系是通过类所实现的方法在编译期推断出来的,如果我们定义一个空接口的话,那么显然所有的类都实现了这个接口,反过来,我们也可以通过空接口来指向任意类型,从而实现类似 Java 中 Object 类所承担的功能,而且显然 Go 的空接口实现更加简洁,通过一个简单的字面量即可完成:

interface{}

注意:空接口和接口零值不是一个概念,前者是 interface{},后者是 nil。

10.反射和泛型

(1)Java支持反射,最典型的应用场景就是IOC容器。
(2)Go 也支持反射功能,并且专门提供了一个 reflect 包用于提供反射相关的 API。reflect 包提供的两个最常用、最重要的类型就是 reflect.Type 和 reflect.Value。前者用于表示变量的类型,后者用于存储任何类型的值,分别可以通过 reflect.TypeOf 和 reflect.ValueOf 函数获取。
(3)Java自1.5之后支持泛型。
(4)Go没有在语言层面支持泛型,可以通过空接口结合反射实现。

二、错误处理

1.error类型

(1) Java中Throwable是所有错误或异常的超类,其下又分为Error和Exception两个大的分类。Error错误,表示Java系统内部和资源耗尽的错误,不会抛出该类异常,只会告知用户然后终止程序运行。Exception又分作运行时异常RuntimeException和检查异常CheckedException,运行时异常是正常抛出的错误,而检查异常是外部错误,需要程序去捕获。
(2)Go 语言为错误处理定义了一个标准模式,即 error 接口,其中只声明了一个 Error() 方法,用于返回字符串类型的错误消息:

type error interface {
	Error() string
}

关于自定义并返回 error 类型错误信息,可以通过 Go 标准错误包 errors 提供的 New() 方法快速创建一个 error 类型的错误实例。

2.defer语句

Go中的defer语句相当于Java中的final语句,不同的是,defer语句的个数没有限制,而且最后执行时按照后入先出的顺序执行所有的defer语句。
defer语句要写在方法最前面,防止后面的代码中断执行后程序感知不到。
defer语句后也可以加一个匿名函数来执行复杂的语句。

defer func() {
	// 执行复杂的清理工作...	
}()

3.panic和recover

当代码运行时出错,而又没有在编码时显式返回错误时,Go 语言会抛出 panic,类似Java的RuntimeException.
无论是 Go 语言底层抛出 panic,还是我们在代码中显式抛出 panic,处理机制都是一样的:当遇到 panic 时,Go 语言会中断当前协程(即 main 函数)后续代码的执行,然后执行在中断代码之前定义的 defer 语句(按照先入后出的顺序),最后程序退出并输出 panic 错误信息,以及出现错误的堆栈跟踪信息。
还可以通过 recover() 函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,而是继续可以执行后续代码,实现类似 Java中 try…catch 语句的功能。
可以类比为 panic、recover、defer 组合起来实现了传统面向对象编程异常处理的 try…catch…finally 功能。

三、项目开发

1.编译特点

Go和Java一样,是编译型的静态语言,但Java的特点“一次编译,到处运行”,是因为Java编译成为.class文件之后,由虚拟机解释成二进制文件执行,Java程序运行在虚拟机上,虚拟机屏蔽了平台差异,同时也带来了限制,Java程序的运行要经过两个步骤,而且必须要运行在JVM上。而Go语言则是直接编译成二进制可执行文件,比较灵活。

2.项目管理

Go使用Go Modules来管理代码,类似Java中的Maven。Go Modules的项目根目录下默认包含了一个go.mod文件,通过这个文件管理项目依赖,类似Maven中的pom.xml文件。下图为例calc项目结构,使用go build构建项目。
calc项目结构

3.程序入口

Go的程序入口文件为main.go,这个文件必须在main包中。Java中程序入口为类中的main方法,如一
个类中没有main方法,则该类无法独立运行。

4.单元测试

Go在单元测试需要引入testing包,类似于Java中的JUnit。

5.包

Go使用包作为基本单位来管理代码,每个 Go 源代码文件的开头都是一个 package 声明,表示该文件中 Go 代码所属的包。包是 Go 语言里最基本的分发单位,也是工程管理中依赖关系的体现。有些类似Java中包+类的作用。

6.配置路径

在引入 Go Modules 以前,Go 语言会基于 GOPATH 这个系统环境变量配置的路径为根目录(可能有多个),然后依次去对应路径下的 src 目录下根据包名查找对应的文件目录,如果目录存在,则再到该目录下的源文件中查找对应的变量、类属性、函数和成员方法。

在启用 Go Modules 之后,不再依赖 $GOPATH 定位包,而是基于 go.mod 中 module 配置值作为根路径,在该模块路径下,根据包名查找对应目录,如果存在,则继续到该目录下的源文件中查找对应变量、类属性、函数和成员方法。

7.包的引用

在 Go 语言中,可以通过 import 关键字导入官方提供的包、第三方包、以及自定义的包,导入第三方包时,还需要通过 go get 指令下载才能使用,如果基于 Go Modules 管理项目的话,这个依赖关系会自动维护到 go.mod 中。

四、数据类型

1.变量声明

Go和Java都是强类型语言,不同的是,Java在声明变量时有严格的格式和类型,而Go则比较随意,通过var来声明变量,也可以使用:=运算符声明并初始化变量(此时可以省略var),虽然看起来和动态语言声明变量类似,但是Go也是强类型语言,底层会自动根据赋值判断对应变量的类型,这个判断过程是在编译期做的,不是运行期,所以Go也是静态语言。

var v1 int = 10 //方式一,常规的初始化操作
var v2 = 10     //方式二,此时变量类型会被编译器自动推导出来
v3 := 10        //方式三,可以省略var,编译器可以自动推导出v3的类型

如上所示,Go对变量的声明,类型放在变量名之后,而且可以省略,Java类型在变量名之前,不可省略。

2.语句结束

所有的Go语句不需要分号作为结束符,Java语句结束必须加分号。

3.驼峰命名法

Java和Go的变量命名均遵循驼峰命名法。

4.多重赋值

Go支持多重赋值,例如,交换i,j变量,如果在Java中需要一个中间变量来实现,在Go中只需要i, j = j, i;这一句即可。

5.匿名变量

Go支持匿名变量,可使用_接受不想要的变量,会直接抛弃。

6.常量

Go通过const关键字定义常量,常量类型可以指定也可以省略(由底层自动推导)。Java通过static final组合关键字定义常量,类型不可省略。

const Pi float64 = 3.14159265358979323846
const zero = 0.0 //无类型浮点常量
const (          //通过一个const关键字定义多个常量,和var类似
	size int64 = 1024
	eof = -1  // 无类型整型常量
)
const u,v float32 = 0, 3 // u = 0.0, v = 3.0 ,常量的多重赋值
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 无类型整型和字符串常量

7.预定义常量

Go预定义的常量:true、false、iota。iota是Java中没有的,可以被认为是一个可被编译器修改的常量,在每一个 const 关键字出现时被重置为 0,然后在下一个 const 出现之前,每出现一次 iota,其所代表的数字会自动增 1。

8.枚举

Java中枚举的关键字是enum,在Go中不支持enum关键字,通过在 const 后跟一对圆括号定义一组常量的方式来实现枚举。

9.变量修饰符

Go中的变量和常量不用public和private修饰,通过首字母大小来判断是否包内可见。首字母大写相当于Java中用public修饰包外也可以访问,首字母小写相当于用private修饰,只能包内访问。

10.基本数据类型

Go支持的基本数据类型:

  • 布尔类型:bool
  • 整型:int8、byte、int16、int、uint、uintptr 等
  • 浮点类型:float32、float64
  • 复数类型:complex64、complex128
  • 字符串:string
  • 字符类型:rune
  • 错误类型:error

(1)Go支持的整型类型非常丰富,其中int、uint、uintptr的位数与平台有关。

(2)Go在运算时无法进行自动类型转换,不同类型的值进行运算会报错。

数据类型之间的互相转化,只需调用要转化的数据类型对应的函数即可。(在有符号与无符号以及高位数字向低位数字转化时,需要注意数字的溢出和截断。)

目前 Go 语言不支持将数值类型转化为布尔型,需要自己根据需求去实现类似的转化。

(3)Go语言中++ 或 – 只能出现在语句中,不能用于表达式,而且只能放在变量后,不能放在变量前。

(4)Go中float32相当于Java中的float,Go中的float64相当于Java中的double。

(5)Go支持复数类型,复数支持两种类型:complex64(32 位实部和虚部) 和 complex128(64 位实部与虚部),对应的表示示例如下,和数学概念中的复数表示形式一致:

var complexValue1 complex64
complexVaule1 = 1.10 + 10i           //由两个float32实数构成的复数类型
complexVaule2 := 1.10 + 10i          //和浮点型一样,默认自动推导的实数类型是float64,所以complexVaule2 是 complex128类型
complexVaule3 := complex(1.10, 10)   //与 complexValue2 等价

对于一个复数 z = complex(x, y),就可以通过 Go 语言内置函数 real(z) 获得该复数的实部,也就是 x,通过 imag(z) 获得该复数的虚部,也就是 y。

(6)Go中字符串是一种基本数据类型,Java中字符串是引用类型。

Go的字符串默认是通过 UTF-8 编码的字符序列,当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节,比如中文编码通常需要 3 个字节。Java中的字符串底层是指针指向的一个字符数组。

Go和Java一样,字符串可以通过数组下标访问其中的字符,而且不可变值。

str := "Hello world"
str[0] = 'X' //编译错误

Go 语言中字符串默认是 UTF-8 编码的 Unicode 字符序列

在 Go 语言中,可以通过字符串切片实现获取子串的功能:

str := "hello, world"
str1 := str[:5]  // 获取索引5(不含)之前的子串
str2 := str[7:]  // 获取索引7(含)之后的子串
str3 := str[0:5] // 获取从索引0(含)到索引5(不含)之间的子串
fmt.Println("str1:", str1)
fmt.Println("str2:", str2)
fmt.Println("str3:", str3)

切片相当于Java中的subString方法,取元素的区间同样是左闭右开。

Go 语言对字符串中的单个字符进行了单独的类型支持,在 Go 语言中支持两种字符类型:

  • 一种是 byte,代表 UTF-8 编码中单个字节的值(它也是 uint8 类型的别名,两者是等价的,因为正好占据 1 个字节的内存空间);
  • 另一种是 rune,代表单个 Unicode 字符(它也是 uint32 类型的别名,因为正好占据 4 个字节的内存空间。

如果想要将 Unicode 字符编码转化为对应的字符,可以使用 string 函数进行转化。

UTF-8 编码不能这样转化,英文字符没问题,因为一个英文字符就是一个字节,中文字符则会乱码,因为一个中文字符编码需要三个字节,转化单个字节会出现乱码。

(7)Go中整型数据可以通过 Unicode 字符集转化为对应的 UTF-8 编码的字符串:

v1 := 65
v2 := string(v1) //v2 = A
//Unicode 兼容 ASCII 字符集,所以 65 被转化为 A。
v3 := 30028
v4 := string(v3) //v4 = 界

此外,还可以将 byte 数组或者 rune 数组转化为字符串,因为字符串底层就是通过这两个基本字符类型构建的:

v1 := []byte{'h','e','l','l','o'}
v2 := string(v1)  // v2 = hello
v3 := []rune{0x4e2d, 0x56fd, 0x4eba)
v4 := string(v3)  // v3 = 中国人  

当然了,byte 是 uint8 的别名,rune 是 uint32 的别名,所以也可以看做是整型数组和字符串之间的转化。

Java中整型数据可以通过String.valueOf()方法转化为字符串。

(8)Go 语言默认不支持将字符串类型强制转化为数值类型,即使字符串中包含数字也不行。如果要实现更强大的基本数据类型与字符串之间的转化,可以使用 Go 官方 strconv 包提供的函数。

Java可以通过Integer.parseInt()方法将包含数字的字符串转化为整型数据。

11.复合类型

Go支持的复合类型:

  • 指针(pointer)
  • 数组(array)
  • 切片(slice)
  • 字典(map)
  • 通道(chan)
  • 结构体(struct)
  • 接口(interface)

(1)对于数组,Go和Java很类似,都是固定长度的、同一类型的数据集合。Go一些常见的数组声明方法如下所示:

var a [8]byte // 长度为8的数组,每个元素为一个字节
var b [3][3]int // 二维数组(9宫格)
var c [3][3][3]float64 // 三位数组(立体的9宫格)
var d = [3]int{1,2,3} // 声明时初始化
var e = new{[3]string} // 通过new初始化

Go 语言还提供了一个关键字 range用于遍历数组

for i, v := range arr {
	fmt.Println("Element", i, "of arr is", v)
}

range 表达式返回两个值,第一个是数组下标索引值,第二个是索引对应数组元素值。

(2)Go中的数组是值类型,切片是引用类型,支持动态的添加新元素。

在 Go 语言中,切片是一个新的数据类型,与数组最大的不同在于,切片的类型字面量中只有元素的类型,没有长度:

数组和切片

切片从底层管理上来看依然使用数组来管理元素,可以看作是对数组做了一层简单的封装。

创建切片的方法主要有三种 —— 基于数组(通过 array[start:end] 这样的方式基于数组生成一个切片)、切片和直接创建(Go 语言提供的内置函数 make() 可以用于灵活地创建切片)。最终切片都是基于数组创建的,切片可以看做是操作数组的指针。

切片底层结构:

切片底层引用了一个数组,由三个部分构成 —— 指针、长度和容量,指针指向数组起始下标,长度对应切片中元素的个数,容量则是切片起始位置到底层数组结尾的位置:

切片的结构

可以通过 append() 函数向切片追加新元素,append() 的第二个参数是一个不定参数,可以是若干个元素,也可以是一个另一个切片。如果追加的元素超过了原切片的容量,底层会自动扩容(默认两倍)。

切片可以通过copy()函数复制,默认按照小切片的个数进行复制。

切片结构体:

type slice struct{
	array unsafe.Pointer  //指向存放数据的数组指针
	len   int             //长度有多大
	cap   int             //容量有多大
}

两个切片指向同一个数组会存在内存共享问题,解决办法是将其中一个切片扩容到比原容量大,则会自动新建一个底层数组。

排序可以使用sort包。

(3)Go字典类型的声明:

var testMap map[string]int

testMap := map[string]int{
	"one":1,
	"two":2,
	"three":3,
}

或者类似切片,使用Go内置的make方法:

var testMap = make(map[string]int)

这种方法的初始化可以使用下图所示方法添加键值对:

testMap["one"] = 1
testMap["two"] = 2
testMap["three"] = 3

字典查找返回两个值,找到的键值和是否成功找到的标识

value, ok := testMap["one"]
if ok { // 找到了
	//处理找到的value
}

Go在声明字典的键类型时,要求数据类型必须是支持通过 == 或 != 进行判等操作的类型。在Java中Map类型的键值要求必须重写equals()和hashcode()方法。

Go用内置函数 delete()删除容器内的元素,Java用remove()。

(3)Go支持指针,Java不支持,但Java的引用类型类似指针。Java中无指针,所以程序员不能直接操作内存地址,这在一定程度上也保证了安全性。

声明指针:var ptr *int // 声明指针类型

*ptr取指针指向的变量值,&a取变量的地址。

Go规定unsafe.Pointer 是特别定义的一种指针类型,它可以包含任意类型变量的地址(类似 C 语言中的 void 类型指针)。Go 官方文档对这个类型有如下四个描述:

a.任何类型的指针都可以被转化为 unsafe.Pointer;

b.unsafe.Pointer 可以被转化为任何类型的指针;

c.uintptr 可以被转化为 unsafe.Pointer;

d.unsafe.Pointer 可以被转化为 uintptr。

uintprt类型单独列出的原因是:uintptr 是 Go 内置的可用于存储指针的整型,而整型是可以进行数学运算的!因此,将 unsafe.Pointer 转化为 uintptr 类型后,就可以让本不具备运算能力的指针具备了指针运算能力(如:指向前一个元素的指针加上元素偏移量可以得到下一个元素的内存地址)。实际使用时要尽量避免,因为可以绕过 Go 指针的安全限制,实现对指针的动态偏移和计算了,这会导致即使发生数组越界了,也不会报错,而是返回下一个内存地址存储的值,这就破坏了内存安全限制,所以这也是不安全的操作。

五、流程控制

1.Go的if条件语句不用加括号,但函数体就算只有一句语句括号也不可省略。Go语句后的{必须与if条件语句在同一行。
2.Go的switch case不需要显式通过 break 语句退出某个分支,上一个分支语句代码会在下一个 case 语句出现之前自动退出,不会像Java中出现击穿现象。如果想要继续执行后续分支代码,可以通过一个 fall through 语句来声明。可以不设定 switch 之后的条件表达式,在这种情况下,整个 switch 结构与多个 if…else… 的逻辑作用等同。
3.Go的循环语句只支持for关键字,不支持while和do-while。for循环条件中也支持多重赋值。
4.对于可迭代的集合(数组、切片、字典),Go 语言还支持通过 for-range 结构对其进行循环遍历。

for k, v := range a {
	fmt.Println(k, v)
}

该循环结构的便利之处在于可以同时取出索引/键及对应的值。
5.Go 语言的 for 循环同样支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环。
6.Go 语言的 break 和 continue 与其他语言的不同之处在于支持与标签结合跳转到指定的标签语句,从而改变这两个语句的默认跳转逻辑,标签语句通过标签 + :进行声明。

arr := [][]int{{1,2,3},{4,5,6},{7,8,9}}
ITERATOR1:
for i := 0; i < 3; i++ {
	for j := 0; j < 3;j++ {
		num := arr[i][j]
		if j > 1 {
			break ITERATOR1
		}
		fmt.Println(num)
	}
}

7.Go支持goto关键字,表示跳转到本函数内的某个标签。

六、函数

在 Go 语言中,函数主要有三种类型:

普通函数
匿名函数(闭包)
类方法

1.普通函数

(1)Go 普通函数的基本组成包括:关键字 func、函数名、参数列表、返回值、函数体和返回语句。与Java类似。
Go的普通函数构成

(2)函数调用,如果是同一个包内的函数,直接调用即可。如果是不同包,需先导入该函数所在的包,然后才能调用该函数。

package main

import (
	"fmt"
	"mymath"
)

func main(){
	fmt.Println(mymath.Add(1,2))      // 3
}

在调用其他包定义的函数时,只有函数名首字母大写的函数才可以被访问。

(3)函数的参数传递除了与Java类似的按值传参和引用传参之外,还有变长传参。
所谓变长参数指的是函数参数的数量不确定,可以按照需要传递任意数量的参数到指定函数,如fmt.Pringf()函数。
只需要在参数类型前加上 … 前缀,就可以将该参数声明为变长参数:

func myfunc(numbers ...int) {
	for _, number := range numbers {
		fmt.Println(number)
	}
}
func Printf (format string, args ...interface{}) {
	// ...
}

指定变长参数类型为 interface{},就可以支持传递任意类型的值作为变长参数。
Go 语言并没有在语法层面提供对泛型的支持,所以只能通过反射和 interface{} 类型实现。
Go函数的一大特点就是支持多返回值。此外,在设置多返回值时,还可以对返回值进行变量命名。

2.匿名函数

(1)匿名函数是一种没有指定函数名的函数声明方式。
Go 匿名函数也可以赋值给一个变量或者直接执行:
匿名函数不能独立存在,会报编译错误,它必须赋值给一个变量,即把函数的地址赋值给一个变量,然后通过这个变量来调用这个匿名函数

//1、将匿名函数赋值给变量
add := func(a, b int) int {
	return a + b
}

//调用匿名函数 add
fmt.Println(add(1,2))

//2.定义时直接调用匿名函数
func(a, b int){
	fmt.Println(a + b)
}(1, 2)

(2)闭包:指的是引用了自由变量(未绑定到特定对象的变量,通常在函数外定义)的函数,被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的上下文环境也不会被释放(比如传递到其他函数或对象中)。
显然,闭包只能通过匿名函数实现,我们可以把闭包看作是有状态的匿名函数,反过来,如果匿名函数引用了外部变量,就形成了一个闭包(Closure)。
(3)匿名函数除了可以赋值给普通变量外,还可以作为参数传递到函数中进行调用,就像普通数据类型一样.
(4)还可以将匿名函数作为函数返回值。

3.高阶函数

(1)所谓高阶函数,就是接收其他函数作为参数传入,或者把其他函数作为结果返回的函数。
(2)装饰器模式(Decorator)是一种软件设计模式,其应用场景是为某个已经存在的功能模块(类或者函数)添加一些「装饰」功能,而又不会侵入和修改原有的功能模块。
(3)在Go语言中通过高阶函数实现装饰器模式:
例:乘法运算函数

packate main

import "fmt"

func multiply(a, b int) int {
	a := 2
	b := 8
	c := multiply(a, b)
	fmt.Printf("%d × %d = %d\n", a, b, c)
}

引入装饰器模式实现计算乘法运算的执行时间

package main

import (
	"fmt"
	"time"
)

//为函数类型设置别名提高代码可读性
type MultiPlyFunc func(int, int) int

//乘法运算函数
func multiply(a, b int) int {
	return a * b
}

//通过高阶函数在不侵入原有函数实现的前提下计算乘法函数执行时间
func execTime(f MultiPlyFunc) MultiPlyFunc {
	return func(a, b int) int {
		start := time.Now() //起始时间
		c := f(a, b)  //执行乘法运算函数
		end := time.Since(start) //函数执行完毕耗时
		fmt.Printf("--- 执行耗时:%v ---\n", end)
		return c  //返回计算结果
	}
}

func main(){
	a := 2
	b := 8
	// 通过修饰器调用乘法函数,返回的是一个匿名函数
	decorator := execTime(multiply)
	// 执行修饰器返回函数
	c := decorator(a, b)
	fmt.Printf("%d × %d = %d\n", a, b, c)
}

4.优化递归函数性能

递归函数耗时大的原因:a.递归函数调用过程中存在大量重复计算。b.递归函数本身的特点,调用过程中方法压栈造成的指数级消耗。

(1)通过内存缓存技术优化递归函数性能

通过缓存中间计算结果来避免重复计算——动态规划。

(2)通过尾递归优化递归函数性能

尾调用是指一个函数的最后一个动作是调用一个函数(只能是一个函数调用,不能有其他操作,比如函数相加、乘以常量等)。若这个函数在尾位置调用自身,则称这种情况为尾递归。
它是尾调用的一种特殊情形。尾调用的一个重要特性是它不是在函数调用栈上添加一个新的堆栈帧 —— 而是更新它,尾递归自然也继承了这一特性,这就使得原来层层递进的调用栈变成了线性结构,因而可以极大优化内存占用,提升程序性能,这就是尾递归优化技术。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值