从 Java 到 Go:面向对象的巨人与云原生的轻骑兵
代码组织的基本单元
在 Java 中,我们会创建 .java 文件作为 类(类名与文件名相同),并在该类中定义相关的字段或方法等(OOP),如下定义 User 和 Address 相关的内容便需要声明两个 .java 文件(User.java, Address.java)定义类:
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Address {
private String city;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
而在 Go 语言中,它是通过 “包” 来组织代码的:每个目录下的所有 .go 文件共享同一个 包,在包内可以定义多个结构体、接口、函数或变量。它并不要求文件名与声明的内容一致,比如创建 User “结构体” 并不会要求 .go 文件也命名为 User.go,而是任何命名都可以(命名为 user.go 甚至 a.go 这种无意义的命名),而且同一个包下可以创建多个 .go 文件。如下为在 user 包下定义 User 和 Address 相关的内容,它们都被声明在一个 user.go 文件中:
package user
type User struct {
name string
}
func (u *User) Name() string {
return u.name
}
func (u *User) SetName(name string) {
u.name = name
}
type Address struct {
city string
}
func (a *Address) City() string {
return a.city
}
func (a *Address) SetCity(city string) {
a.city = city
}
相比来说,Java 代码组织的基本单元是类,作为面向对象的语言更侧重对象定义,而 Go 代码组织的基本单元是包,并更侧重功能模块的聚合。
可见性控制
在 Java 中通过 public/protected/private 关键字控制成员的可见性,而在 Go 语言中,通过 首字母大小写 控制 “包级别的导出”(大写字母开头为 public),包的导出成员对其他包可见。以 user 包下 User 类型的定义为例,在 main 包下测试可见性如下:
package main
import (
"fmt"
// user package 的全路径
"learn-go/src/com/github/user"
// 不能导入未使用到的包
//"math"
)
func main() {
var u user.User
// 在这里是不能访问未导出的字段 name
// fmt.Println(u.name)
fmt.Println(u.Name())
}
Go 语言不能导入未使用到的包,并且函数是基于包的一部分。比如 fmt.Println 函数,这个函数是在 fmt 包下的,调用时也是以包名为前缀。
变量的声明
在 Java 语言中,对变量(静态变量或局部变量)的声明只有一种方式,“采用 = 运算符赋值” 显式声明(在 Jdk 10 + 支持 var 变量声明),如下:
public class Test {
public static void main(String[] args) {
int x = 100;
}
}
而在 Go 语言中,变量声明有两种主要方式:短声明(:= 运算符) 和 长声明(var 声明),它们的适用场景和限制有所不同,以下是详细区分:
短声明(:=)
只能在函数(包括 main、自定义函数或方法、if/for 块等)内部使用,不能在包级别(全局作用域)使用,并且 声明的局部变量必须被使用,不被使用的局部变量不能被声明:
package main
import "fmt"
func main() {
// 正确
x := 10
fmt.Println(x)
// 未被使用,不能被声明
// y := 20
// 不赋值也不能被声明
// z :=
}
// 错误:不能在包级别使用短声明
// y := 20
这种短声明直接根据右侧值自动推断变量类型,无需显式指定类型,并且可以一次性声明多个变量,但至少有一个变量是 新声明的:
package main
import "fmt"
func main() {
// 同时声明 a 和 b
a, b := 1, "abc"
// c 是新变量,b 被重新赋值
c, b := 2, "def"
// 无新变量无法再次对已声明的变量再次声明
//a, b := 4, "error"
fmt.Println(a, b, c)
}
长声明(var 声明)
在全局作用域声明变量必须使用 var;在需要延迟初始化时也需要采用长声明;显示指定类型也需要使用长声明
package main
import "fmt"
var global int = 42
func main() {
// a = 0
var a int
// s = ""
var s string
// 未被初始化值会默认为“零”值,a 为 0,s 为空字符串
fmt.Println(a, s)
}
函数内部的局部变量,尤其是需要类型推断和简洁代码时优先用短声明;在包级别声明变量,需要显式指定类型或声明变量但不立即赋值(零值初始化)时,使用长声明。
在 Go 语言中还有一点需要注意:声明变量时,应确保它与任何现有的函数、包、类型或其他变量的名称不同。如果在封闭范围内存在同名的东西,变量将对它进行覆盖,也就是说,优先于它,如下所示:
package main
import "fmt"
func main() {
// 这个变量会把导入的 fmt 包覆盖掉
fmt := 1
println(fmt)
}
那么我们导入的 fmt 包在被局部变量覆盖后便不能再被使用了。
常量的声明
Go 语言中对常量的声明采用 const 关键字,并且在声明时便需要被赋值,如下所示:
package main
import "fmt"
// DaysInWeek const 变量名 类型 = 具体的值
const DaysInWeek int = 7
func main() {
const name = "abc"
fmt.Println(name, DaysInWeek)
}
在 Java 语言中对常量的声明会使用 static final 引用:
public class Constants {
public static final int DAYS_IN_WEEK = 7;
// ...
}
方法 / 函数的声明
在 Go 语言中,方法的声明遵循 func (接收器) 方法名 (入参) 返回值 的格式,无返回值可以不写(无需 void 声明),通过 接收器(Receiver) 将方法绑定到结构体上,如下为 User 结构体方法的声明:
package user
type User struct {
name string
}
// Name (u *User) 即为接收器,表示该方法绑定在了 User 类型上
func (u *User) Name() string {
return u.name
}
func (u *User) SetName(name string) {
u.name = name
}
而 “函数” 的声明不需要定义接收器,遵循的是 func 方法名 (入参) 返回值 的格式。Go 语言中的函数类似于 Java 语言中的静态方法,以下是声明将整数扩大两倍的函数:
package main
func double(a *int) {
*a *= 2
}
并且,在 Go 语言中,方法 / 函数支持多返回值(常用于错误处理),并且如果并不需要全部的返回值,可以用 _ 对返回值进行忽略,因为 Go 语言不允许定义未使用的局部变量,如下所示:
package main
import "fmt"
func main() {
// 忽略掉了第三个返回值
s1, s2, _, e := multiReturn()
if e == nil {
fmt.Println(s1, s2)
}
}
func multiReturn() (string, string, string, error) {
return "1", "2", "2", nil
}
此外,接收器参数和函数的形参支持传入指针,用 * 符号表示。在 Go 语言中有指针的概念,我们在这里说明一下:Go 语言是 “值传递” 语言,方法 / 函数的形参(或接收器)如果不标记指针的话,接收的实际上都是 实参的副本,那么 在方法 / 函数中的操作并不会对原对象有影响。如果想对原对象进行操作,便需要通过指针获取到原对象才行(因为值传递会对原对象和形参对象都划分空间,所以针对较大的对象都推荐使用指针以节省内存空间)。在如下示例中,如果我们将上文中 double 方法的形参修改为值传递,这样是不能将变量 a 扩大为两倍的,因为它操作的是 a 变量的副本:
package main
import "fmt"
func main() {
a := 5
double(a)
// 想要获取 10,但打印 5
fmt.Println(a)
}
func double(a int) {
a *= 2
}
想要实现对原对象 a 的操作,便需要使用指针操作,将方法的声明中传入指针变量 *int:
package main
import "fmt"
func main() {
a := 5
// & 为取址运算符
double(&a)
// 想要获取 10,实际获取 10
fmt.Println(a)
}
// *int 表示形参 a 传入的是指针
func double(a *int) {
// *a 表示从地址中获取变量 a 的值
*a *= 2
}
再回到 User 类型的声明中,如果我们将接收器修改成 User,那么 SetName 方法是不会对原变量进行修改的,它的修改实际上只针对的是 User 的副本:
package user
type User struct {
name string
}
// SetName 指定为值接收器
func (u User) SetName(name string) {
u.name = name
}
这样 SetName 方法便不会修改原对象,SetName 的操作也仅仅对副本生效了:
package main
import (
"fmt"
"learn-go/src/com/github/user"
)
func main() {
u := user.User{}
u.SetName("abc")
// 实际输出为 {},并没有对原对象的 name 字段完成赋值
fmt.Println(u)
}
在 Java 中并没有指针的概念,Java 中除了基本数据类型是值传递外,其他类型在方法间传递的都是 “引用”,对引用对象的修改也是对原对象的修改。
接口
Go 语言也支持接口的声明,不过相比于 Java 语言它更追求 “灵活与简洁”。Go 的接口实现是 “隐式地”,只要类型实现了接口的所有方法,就自动满足该接口,无需显式声明。如下:
package writer
type Writer interface {
Write([]byte) (int, error)
}
// File 无需声明实现 Writer,实现了接口所有的方法便自动实现了该接口
type File struct{}
func (f *File) Write(data []byte) (int, error) {
return len(data), nil
}
Java 语言则必须通过 implements 关键字声明类对接口的实现:
public interface Writer {
int write(byte[] data);
}
public class File implements Writer { // 必须显式声明
@Override
public int write(byte[] data) {
return data.length;
}
}
它们对类型的判断也是不同的,在 Go 语言中采用如下语法:
package writer
func typeTransfer() {
var w Writer = File{}
// 判断是否为 File 类型,如果是的话 ok 为 true
f, ok := w.(File)
if ok {
f.Write(data)
}
}
而在 Java 语言中则采用 instanceof 和强制类型转换:
private void typeTransfer() {
Writer w = new File();
if (w instanceof File) {
File f = (File) w;
f.write(data);
}
}
Go 语言还采用空接口 interface{} 来表示任意类型,作为方法入参时则支持任意类型方法的传入,类似 Java 中的 Object 类型:
package writer
func ProcessData(data interface{}) {
// ...
}
除此之外,Go 语言在 1.18+ 版本引入了泛型,采用 [T any] 方括号语法定义类型约束,any 表示任意类型,如果采用具体类型限制则如下所示:
package writer
// Stringer 定义约束:要求类型支持 String() 方法
type Stringer interface {
String() string
}
func ToString[T Stringer](v T) string {
return v.String()
}
通过类型的限制便能使用类型安全替代空接口 interface{},避免运行时类型断言:
// 旧方案:空接口 + 类型断言
func OldMax(a, b interface{}) interface{} {
// 需要手动断言类型,易出错
}
// 新方案:泛型
func NewMax[T Ordered](a, b T) T { /* 直接比较 */ }
泛型还在通用数据结构上有广泛的应用:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}更多推荐



所有评论(0)