前言:知识有限,文中有错误或者不足的地方,欢迎大家指正,补充,同时,我们要养成好习惯,做一个可以免费给别人点赞的程序员,而不是一个喜欢收藏吃灰的程序员。
基本语法格式
Golang: 编码风格相对统一,简单,没有太多的语法糖等,Java层次清晰,全面面向对象。
在Java或者PHP、Python中,声明了变量,可以不使用,也不报错。
private static String CToString(int n) { int data = n; return String.valueOf(n);}
在Golang中,变量申明以后,必须被使用,或者使用_标明,否则编译会报错,一般来讲,我们都不会申明不使用的变量了。
func CToStringPanic(n int) string { newData := n // newData编译无法通过 return strconv.Itoa(n)}func CToStringOk(n int) string { _ := n // ok return strconv.Itoa(n)}
在Java中,如果内部申明变量,但是没有初始化,有时候会编译报错,需要注意,例如以下代码;
public void Cpainc() { int salary; Object object; System.out.println(salary); // 编译错误 System.out.println(object); // 编译错误}
在Golang中:对于基本类型来讲,声明即初始化,对于引用类型,声明则初始化为nil。
Java: 对方法、变量及类的可见域规则是通过private、protected、public关键字来控制的,类似PHP
Golang: 控制可见域的方式只有一个,当字段首字母开头是大写时说明其是对外可见的、小写时只对包内成员可见。
//公开type Public struct{ Name string //公开 Age uint8 //公开 salary uint //私有}//私有type private struct{ name string //私有 age uint8 //私有 salary uint //私有}
结构体声明及使用
先看Golang的:
// 定义people结构体type People struct { Title string Age uint8}// 使用func main() { P := new(People) // 通过new方法创建结构体指针 person1 := People{} person2 := People{ Title: "xiaoHong", Age: 18, } ...........}
再Java的:
方法和函数是有区别的
在Java中:所有的“函数”都是基于“类”这个概念构建的,也就是只有在“类”中才会包含所谓的“函数”,这里的“函数”被称为“方法”,可见上方声明实体并使用。
在Golang中:“函数”和“方法”的最基本区别是:函数不基于结构体而是基于包名调用,方法基于结构体调用。如下实例:
Java中的指针和数据类型:
Go中的指针和数据类型:
Java和Go在指针和数据类型的处理方式上存在明显差异。Java采用引用类型,不支持指针操作,而Go在保持安全性的同时,允许显式的指针操作,并将基本数据类型和特定引用类型区分对待。这些差异在编程时需要注意,以确保正确处理数据和内存管理。
数组对比
在Java中:当向方法中传递数组时,可以直接通过该传入的数组修改原数组内部值(浅拷贝)。
在Golang中:则有两种情况:在不限定数组长度(为slice)时也直接改变原数组的值,当限定数组长度时会完全复制出一份副本来进行修改(深拷贝):
Java:
public static void main(String[] args) { int[] arr = {11, 21, 31}; c(arr); System.out.println(Arrays.toString(arr)); // 2,21,31 } private static void c(int[] a r r) { arr[0] = 2;}
Golang:
对象也不太一样哦
在Go中,当将对象传递给函数作为参数时,实际上会创建原对象的一个全新拷贝,这个拷贝具有自己独立的内存地址。而在Go中,对象之间的赋值操作会复制对象内存中的内容,这就是为什么即使在修改globalUser之前后,其地址保持不变,但对象的内容却发生了变化。
相比之下,在Java中,当将对象传递给函数时,传递的是原对象的引用的拷贝,这个拷贝仍然指向同一块内存地址。因此,Java对象之间的赋值操作实际上是复制对象的引用,而不是对象的内容。当引用指向不同的地址时,对象的内容也会发生变化。
Golang:
//User 定义User结构体type User struct { Name string Age int}var globalUser = User { "zz", 33,}func modifyUser(user User) { fmt.Printf("user的addr = %p\n",&user) fmt.Printf("globalUser修改前的addr = %p\n",&globalUser) fmt.Println("globalUser修改前 = ",globalUser) // 修改指向 globalUser = user fmt.Printf("globalUser修改后的addr = %p\n",&globalUser) fmt.Println("globalUser修改后 = ",globalUser)}func main() { var u User = User { "kx", 32, } fmt.Printf("传递的参数u的addr = %p\n",&u) modifyUser(u)}
指针的差异常
在Java中,当传递引用类型(例如对象或数组)作为函数参数时,实际上传递的是引用的副本,也可以说是引用的拷贝。这意味着传递的引用仍然指向相同的对象或数组,因此对对象或数组的修改将在原对象或数组上反映出来。
在Go中,确实需要显式传递指针才能操作原对象。如果不传递指针,将只传递对象的副本,对副本的修改不会影响原对象。这是Go语言的设计决策,旨在提供更明确的内存控制和避免意外的副作用。因此,在Go中,需要特别注意传递对象的指针,以确保对原对象的修改能够生效。
Golang的指针:
Java的指针:
面向对象编程,不太一样,这里的不同,很多人都不习惯
在Java中,一般使用抽象类和继承
// 定义抽象类 Animalabstract class Animal { private String name; private int age; public Animal(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } // 抽象方法 bark,子类需要实现 public abstract void bark();}// 定义 Dog 类,继承自 Animalclass Dog extends Animal { public Dog(String name, int age) { super(name, age); } // 实现抽象方法 bark @Override public void bark() { System.out.println("狗在汪汪叫!"); }}
在Go中,可以使用结构体和方法接收者来实现这个场景,以下是一个示例:
package mainimport "fmt"// 定义 Animal 结构体type Animal struct { Name string Age int}// 定义 Animal 结构体的方法func (a Animal) GetName() string { return a.Name}func (a Animal) GetAge() int { return a.Age}// 定义 Dog 结构体,嵌套 Animaltype Dog struct { Animal // 嵌套 Animal 结构体,获得其属性和方法}// 定义 Dog 结构体的方法func (d Dog) Bark() { fmt.Println("狗在汪汪叫!")}func main() { // 创建 Dog 实例 myDog := Dog{ Animal: Animal{ Name: "旺财", Age: 3, }, } // 使用嵌套的 Animal 方法 fmt.Printf("狗的名字:%s\n", myDog.GetName()) fmt.Printf("狗的年龄:%d岁\n", myDog.GetAge()) // 调用 Dog 自身的方法 myDog.Bark()}
在Java中,接口主要用于定义不同组件之间的契约或规范。Java中的接口是一种侵入性接口,这意味着实现接口的类必须显式声明它们实现了特定接口。以下是一个示例,演示如何在Java中管理狗的行为:
// 定义一个 Animal 接口,描述动物的基本行为interface Animal { void eat(); void sleep();}// 定义一个 Dog 类,实现 Animal 接口class Dog implements Animal { @Override public void eat() { System.out.println("狗正在吃东西"); } @Override public void sleep() { System.out.println("狗正在睡觉"); }}public class Main { public static void main(String[] args) { // 创建 Dog 实例 Dog myDog = new Dog(); // 调用实现的接口方法 myDog.eat(); myDog.sleep(); }}
在Go中,可以定义一个接口(Factory)并为其定义一些方法,然后创建一个结构体(CafeFactory)并实现这些方法,以满足接口的要求。以下是一个示例代码,演示如何实现这个场景:
package mainimport "fmt"// 定义 Factory 接口type Factory interface { Produce() string Consume() string}// 定义 CafeFactory 结构体,实现 Factory 接口type CafeFactory struct { Product string}// 实现 Produce 方法func (cf CafeFactory) Produce() string { return "生产了 " + cf.Product}// 实现 Consume 方法func (cf CafeFactory) Consume() string { return "消费了 " + cf.Product}func main() { // 创建 CafeFactory 实例 coffeeFactory := CafeFactory{Product: "咖啡"} // 使用 Factory 接口来调用方法 factory := Factory(coffeeFactory) // 调用 Produce 和 Consume 方法 fmt.Println(factory.Produce()) fmt.Println(factory.Consume())}
Golang的非侵入式接口的优势在于其简洁、高效、以及按需实现的特性。
在Go语言中,不存在类的继承概念,而是只需知道一个类型实现了哪些方法以及每个方法的行为是什么。
在实现类型时,我们只需考虑自己应该提供哪些方法,而不必担心接口需要分解得多细才算合理。接口是由使用方根据需要进行定义的,而不是事先规划好的。
这种方式减少了包的引入,因为多引入外部包意味着更多的耦合。接口是由使用方根据其自身需求来定义的,使用方无需担心是否已经有其他模块定义了类似的接口。
相比之下,Java的侵入式接口优势在于其清晰的层次结构以及对类型的行为有严格的管理。
异常处理,相差很大
public class ExceptionHandlingDemo { public static void main(String[] args) { try { // 可能会引发异常的代码 int result = divide(10, 0); System.out.println("结果是: " + result); } catch (ArithmeticException e) { // 捕获并处理异常 System.out.println("发生了算术异常: " + e.getMessage()); } finally { // 不管是否发生异常,都会执行的代码块 System.out.println("这里是finally块,无论如何都会执行"); } } public static int divide(int dividend, int divisor) { // 尝试执行除法操作 return dividend / divisor; }}
在Golang中,"ok模式"或者叫做"错误值模式"(error value pattern)是一种常见的异常处理方式。
在这种模式中,所有可能引发异常的方法或代码将错误作为第二个返回值返回,程序通常需要对返回值进行判断,如果错误不为空(通常用 if err != nil {} 判断),则进行相应的错误处理,并可能中断程序的执行。这种方式相对于传统的异常处理机制如Java的异常捕获和处理机制来说,更为简洁和直观。
package mainimport ( "fmt" "io/ioutil")// 一个用于读取文件内容的函数,可能会返回错误func readFileContent(filename string) (string, error) { // 尝试打开文件 content, err := ioutil.ReadFile(filename) if err != nil { // 如果发生错误,返回错误信息 return "", err } // 如果没有错误,返回文件内容 return string(content), nil}func main() { // 调用 readFileContent 函数,并检查是否有错误发生 content, err := readFileContent("example.txt") if err != nil { // 处理错误,例如打印错误信息 fmt.Println("发生错误:", err) return // 可以选择中断程序的执行 } // 如果没有错误,打印文件内容 fmt.Println("文件内容:") fmt.Println(content)}
defer:defer是用于延迟执行一段代码的关键字。无论函数是否正常返回或者是否发生了恐慌(panic),defer代码都会在函数退出时执行。这通常用于执行一些必须在函数结束时进行的清理工作,例如关闭文件、释放资源等。defer也可以用来调用函数或匿名函数。它确保在函数结束前执行。
//deferfunc someFunction() { defer fmt.Println("This will be executed last") fmt.Println("This will be executed first")}//paincfunc someFunction() { panic("Something went terribly wrong!")}//recoverfunc someFunction() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() panic("Something went terribly wrong!")}
需要注意的是,defer使用一个栈来维护需要执行的代码,所以defer函数所执行的顺序是和defer声明的顺序相反的。
defer fmt.Println(1) defer fmt.Println(2)defer fmt.Println(3)执行结果:321
recover的作用是捕捉panic抛出的错误并进行处理,需要联合defer来使用,类似于Java中的catch代码块:
func someFunction() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) // 在这里可以采取适当的措施 } }() panic("Something went terribly wrong!")}
注:利用recover处理panic指令,defer必须在panic之前声明,否则当panic时,recover无法捕获到panic。
并发编程,也不太一样
在 Java 中,要获得 CPU 资源并异步执行代码单元,通常需要创建一个实现了 Runnable 接口的类,并将该类的实例传递给一个 Thread 对象来执行。以下是一些示例代码,演示了如何在 Java 中执行异步代码单元:
// 创建一个实现了 Runnable 接口的类class MyRunnable implements Runnable { @Override public void run() { // 在这里编写你的异步代码单元 for (int i = 0; i < 5; i++) { System.out.println("线程执行:" + i); try { Thread.sleep(1000); // 模拟任务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } } }}public class Main { public static void main(String[] args) { // 创建一个 Thread 对象,并传递 MyRunnable 的实例 Thread thread = new Thread(new MyRunnable()); // 启动线程 thread.start(); // 主线程可以继续执行其他任务 for (int i = 0; i < 3; i++) { System.out.println("主线程执行:" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}
在 Golang 中,执行异步代码单元通常不需要创建线程,而是通过创建 goroutine 来实现。Goroutine 是 Go 语言的轻量级线程,可以非常高效地执行并发任务。
要执行异步代码单元,只需将代码包装成一个函数,并使用 go 关键字调用该函数,这将创建一个新的 goroutine 来执行该函数中的代码。以下是一个简单的示例:
package mainimport ( "fmt" "time")// 异步执行的函数func asyncFunction() { for i := 0; i < 5; i++ { fmt.Println("异步执行:", i) time.Sleep(time.Second) // 模拟任务执行时间 }}func main() { // 使用 go 关键字创建一个新的 goroutine 来执行 asyncFunction go asyncFunction() // 主线程可以继续执行其他任务 for i := 0; i < 3; i++ { fmt.Println("主线程执行:", i) time.Sleep(time.Second) } // 等待一段时间以确保异步任务有足够的时间执行 time.Sleep(5 * time.Second)}
Java 和 Golang 在并发编程方面有一些显著的区别,这些区别主要涉及到并发模型、线程管理、内存模型等方面:
并发模型:
Java 使用基于线程的并发模型。在 Java 中,线程是基本的并发单元,开发人员需要显式创建和管理线程。Java提供了java.lang.Thread类以及java.util.concurrent包中的各种工具来处理多线程编程。
Golang 使用 goroutines 和通道(channels)的并发模型。Goroutines 是轻量级的用户级线程,Go语言的运行时系统负责调度它们。通道是用于在不同的 goroutines 之间传递数据的机制。这种模型更简单、更容易使用和更安全。
线程管理:
Java 的线程管理相对复杂。开发人员需要显式创建线程对象、启动线程、停止线程,以及处理线程的同步和协调。Java提供了synchronized关键字和各种锁来处理多线程同步。
Golang 的 goroutine 管理由运行时系统自动处理,开发人员只需要使用 go 关键字启动一个函数作为 goroutine,不需要手动创建和管理线程。Golang 提供了通道来进行 goroutine 之间的通信和同步,这大大简化了并发编程。
内存模型:
Java 使用 Java Memory Model(JMM)来管理多线程程序中的内存访问。JMM 规定了共享变量的可见性和操作的顺序。
Golang 使用了一种更简单和更可预测的内存模型。Golang 的内存模型是顺序一致性(Sequential Consistency)的,不需要开发人员担心复杂的内存可见性问题,因为 goroutines 之间的通信是通过通道进行的,不需要显式的锁来保护共享数据。
并发工具:
Java 提供了大量的并发工具和类库,如 java.util.concurrent 包,用于处理各种并发问题,包括线程池、锁、队列、原子操作等。
Golang 也提供了一些内置的并发工具,如 sync 包中的锁、select 语句用于选择通道操作、goroutine 和通道本身。这些工具相对较少,但足以处理大多数并发场景。
总的来说,Golang的并发编程模型更简单、更安全,适用于构建高并发的分布式系统和网络服务。Java则更适合传统的企业级应用和复杂的多线程场景,但需要更多的线程管理和同步工作。开发人员在选择编程语言时,应根据项目的需求和复杂性来考虑哪种并发模型更适合。
Java 中的 synchronized 关键字和 Golang 中的 Mutex(互斥锁)都是用于实现多线程同步的工具,但它们有一些不同之处。以下是它们的主要区别以及示例代码来说明这些区别。
Java 的 synchronized 关键字:
class SynchronizedExample { private int count = 0; // 同步方法,锁是该对象实例 public synchronized void increment() { count++; } // 同步代码块,锁是指定的对象 public void decrement() { synchronized (this) { count--; } }}
Golang 的 Mutex(互斥锁):
以下是 Golang 中 Mutex 的示例:
package mainimport ( "fmt" "sync")func main() { var mu sync.Mutex count := 0 // 同步代码块,锁定 mu mu.Lock() count++ // 解锁 mu mu.Unlock() fmt.Println("Count:", count)}
修饰结构体:带锁结构体初始化后,直接调用对应的线程安全函数就可以。
相似点:
区别点:
Java 中,条件变量通常通过 Lock 接口的 newCondition 方法创建。
Golang 中,条件变量由 sync.Cond 结构体表示,但需要与 sync.Mutex 结合使用。条件变量是通过 sync.NewCond(&mutex) 来创建的,其中 mutex 是一个互斥锁。
Java 中,条件变量的等待和通知通常是在锁的保护下执行的。Object 的 wait() 方法和 notify()、notifyAll() 方法都在获取锁后才能调用。
Golang 中,sync.Cond 的 Wait() 方法是在获取锁后调用的,但是通知方法 Broadcast() 和 Signal() 是在解锁之后执行的,这意味着通知发出后,等待的 goroutines 需要重新竞争锁。
总的来说,条件变量在 Java 和 Golang 中的概念是相似的,都用于线程或 goroutines 之间的同步和通信。然而,它们在具体的使用方式和时机上存在一些区别,尤其是在 Golang 中,条件变量的通知不需要在锁的保护下执行,这可以避免一些潜在的死锁问题,但也需要开发人员更小心地处理竞争条件。
在Java中:
在Golang中:
总之,Java和Golang都支持CAS操作和原子操作,但在具体的实现和用法上有一些区别。在多线程或多goroutine环境下,这些操作可以确保共享变量的原子性,避免竞态条件和数据竞争问题。在选择哪种语言和库进行多线程编程时,可以根据具体的需求和语言特性来决定。
Java懒汉式单例(Lazy Initialization):
public class LazySingleton { private static LazySingleton instance; private LazySingleton() { // 私有构造函数,防止外部直接实例化 } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; }}
饿汉式单例(Eager Initialization):
public class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() { // 私有构造函数,防止外部直接实例化 } public static EagerSingleton getInstance() { return instance; }}
Golang的Once模式
package singletonimport ( "sync")type Singleton struct { // 在这里定义单例的属性}var instance *Singletonvar once sync.Oncefunc GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} }) return instance}
垃圾回收机制
总之,每种编程语言和运行时环境都有其自己的GC实现和优化目标。Go的GC设计注重简单性和低延迟,而JVM的GC提供了更多的灵活性和优化选项,以适应不同类型的应用程序和工作负载。开发人员在选择编程语言和运行时环境时,需要根据项目的需求来考虑GC的性能和特性。
总之,Java的垃圾回收机制确实是一个庞大而复杂的体系,它的目标是自动管理内存,减轻开发人员的负担,并提供不同的算法和选项以适应不同类型的应用程序和工作负载。这使得Java成为一个非常强大和灵活的编程语言,适用于各种不同的应用场景。Golang GC特征
以下是一个简单的Java示例,演示了垃圾回收的行为:
public class GarbageCollectionDemo { public static void main(String[] args) { // 创建一个对象 MyClass obj1 = new MyClass("Object 1"); // 创建另一个对象 MyClass obj2 = new MyClass("Object 2"); // 让 obj1 不再被引用 obj1 = null; // 强制垃圾回收 System.gc(); // 在这里等待一段时间,以便观察垃圾回收的效果 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}class MyClass { private String name; public MyClass(String name) { this.name = name; } @Override protected void finalize() throws Throwable { System.out.println(name + " is being finalized"); }}
Go语言(Golang)的垃圾回收是一种自动管理内存的机制,用于检测和回收不再被引用的对象,从而释放内存资源。Go的垃圾回收器(GC)是Go运行时的一部分,负责管理内存分配和回收。以下是关于Go垃圾回收的一些要点:
总之,Go语言的垃圾回收是一种自动且并发的机制,它负责管理内存,确保不再被引用的对象被及时回收,从而减少内存泄漏和提高应用程序的性能。Go的GC设计考虑了并发性、低延迟以及内存复用等因素,使其成为编写高性能和高并发应用程序的强大工具。
以下是一个简单的Go示例,演示了垃圾回收的行为:
package mainimport ( "fmt" "runtime" "time")func main() { var obj *MyObject for i := 0; i < 10000; i++ { // 创建一个新的对象,并赋值给 obj obj = NewObject(i) // 手动调用垃圾回收 runtime.GC() // 在这里等待一段时间,以便观察垃圾回收的效果 time.Sleep(time.Millisecond * 100) } // 防止 obj 被编译器优化掉 _ = obj}type MyObject struct { id int}func NewObject(id int) *MyObject { return &MyObject{id}}func (o *MyObject) String() string { return fmt.Sprintf("Object %d", o.id)}
性能与资源使用情况
Java的JIT(Just-In-Time)编译策略和Go的AOT(Ahead-Of-Time)编译策略是两种不同的编译和执行策略,它们在编译和性能方面有一些重要的区别。以下是它们的对比:
1. 编译时机:
2. 性能优化:
3. 启动时间:
4. 开发体验:
综上所述,JIT编译和AOT编译都有其适用的场景。JIT编译通常适用于需要动态性能优化的长时间运行的应用程序,而AOT编译适用于需要快速启动和更稳定性能的应用程序。选择哪种策略取决于应用程序的需求和性能目标。
生态环境
Java在生态系统和框架方面的强大是不可否认的。Spring生态系统的广泛应用确实使Java成为了许多企业级应用程序的首选开发语言之一。Spring框架提供了丰富的功能和模块,包括Spring Boot、Spring MVC、Spring Data、Spring Security等,使Java开发更加高效和便捷。
相比之下,Go语言虽然在近年来的发展中取得了很大的成功,但在生态系统和框架方面相对较新。Go的成功主要体现在其简洁性、高性能和并发性等方面,这使得它在云原生开发、分布式系统和网络编程等领域表现出色。
虽然Go的生态系统不如Java成熟,但Go也有一些知名的框架和库,例如:
尽管Go的生态系统相对较新,但它正在不断发展,并且在一些领域表现出色。在选择编程语言和框架时,通常会根据项目的需求和目标来做出决策。有些项目可能更适合使用Java和Spring,而其他项目可能会更适合使用Go。两者都有自己的优势和特点,取决于你的具体用例。
简单总结