Java选手如何看待Golang

写在前面:翻了很多博客,一直没有Java选手转行golang的学习经验贴,思考很久,写下这篇Java选手怎么看待golang这个冉冉新星。

1.走完所有golang基础之后的感受

(1)最大的不适应有这么几点:

  • ---变量定义时,变量类型放在后面,如var temp string;但是Java是在前面,比如int a;
  • ---新建对象时,在go中常用下面这么几种,make()、new()、&引用传递

在新建map、chan、slice时,我们常用make,可以快速创建,并且赋空值,而new和&常用来新建结构体或者接口时使用,可以快速返回创建的对象的引用。

Go 复制代码
	//对象创建
	var temp1 chan string
	temp1 = make(chan string, 2) //带缓冲区
	var temp2 []int
	temp2 = make([]int, 10)
	var temp3 map[int]string
	temp3 = make(map[int]string)
	
	var temp4 *dto.Woman
	temp4 = &dto.Woman{
		Age1:  0,
		Name1: "",
	}
	temp4.Name("嘿嘿嘿")
	
	var temp5 *dto.Men
	temp5 = new(dto.Men)
	temp5.Name("嘎嘎嘎")

	temp1 <- "12"
	temp2[0] = 1
	temp3[12] = "12"
  • ---接口方面,没有严格的implements接口,只是可以新定义struct,然后实现这个接口的所有方法,相当于间接实现了这个接口。
Go 复制代码
	var temp4 dto.Man
	temp4 = &dto.Woman{
		Age1:  0,
		Name1: "",
	}

tips:

Go 的接口变量本身是一个 双字结构(two-word structure),包含:

  1. 动态类型信息(指向具体类型的元数据)
  2. 动态值(指向实际数据的指针或值)

声明接口类型时,无需声明为指针类型,事实上,接口类型可以指向任何类型,本身接口就可以是一个指针类型

(2)基本类型

golang相比Java,缩减了非常多的API,Java里的List,Set,HashMap全被砍掉,数组进行了保留。

Java基础数据类型有8种,int、long、short、float、double、byte、boolean、char

Golang基础数据类型有7种,整型(6种)byte、int、int8、int16、int32、int64、无符号整型(6种):uint、uint8、uint16、uint32、uint64、uintptr,浮点型(2种)float32、float64,复数类型(2种):complex64,complex128、字符:rune、字符串:string

并且Golang还有一些符合数据类型,如指针、数组,切片(类似Python),结构体(可以和C++的结构体类比,但是我把他理解成Java的类),还有并发编程的channel等

tips:go中的数组类型,在 Go 语言中,数组的大小是类型的一部分,因此不同大小的数组是不兼容的,也就是说 [5]int 和 [10]int 是不同的类型。

int在Go语言中,会根据机器情况是64位还是32位进行变化,建议显示声明int64还是int32

(3)输入输出流

golang的打印函数和c++、python的比较类似,使用printf打印可以格式化

(4)go的函数调用

**go中,函数不基于结构体而是基于包名调用,方法基于结构体调用。**在go中,没有类的概念,struct就类似于类的概念,方法归属于结构体,只有结构体被实例化时,才能调用这个结构体的方法,但是函数如果首字母大写,通过包名,是能够全局调用到的。

在Java中,是没有函数这个概念的,只有方法的概念,而方法都是归属于类的,同样,方法只有类被实例化后,才能被这个实例调用到。

(5)多线程

Java中线程可以继承Thread,实现Runnalbe接口,或者实现Callable接口,或者从线程池创建。Java 语言里解决并发问题靠的是多线程,但线程是个重量级的对象,不能频繁创建、销毁,而且线程切换的成本也很高,我们可以使用线程池维护一些线程的复用,ThreadPollExcutor,另外针对并发问题,Java有各种工具类,放在concurrent包下,例如countdownlatch,rw锁,concurrenthashmap,CopyOnWriteArrayList,ThreadLocalRandom,wait&notify,await&notifyALL,AtomicInteger在内的各种自增原子类,不得不说,Java的生态真的好,各种工具类用到你手软,而且是内置到JDK中!!!无需你自己去挑第三方包

而且从JDK21后开始全面支持虚拟线程,使用也非常简单,API方法完全一样,只是创建的时候加上ofVirtual,下面给出实例

java 复制代码
public class Main {
    public static void main(String[] args) throws InterruptedException {

        long start = System.currentTimeMillis();
        List<Thread> threads = IntStream.range(0, 1_000_000)
                .mapToObj(i -> Thread.ofVirtual()  // 替换为ofPlatform()测试传统线程
//                .mapToObj(i -> Thread.ofPlatform()  // 替换为ofPlatform()测试传统线程
                        .start(() -> {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException ignored) {
                            }
                        }))
                .toList();

        for (Thread thread : threads) {
            thread.join();
        }
        long end = System.currentTimeMillis();
        System.out.println("Total time: " + (end - start) + "ms");
    }
}

但是,go的简化设计,虽然没有Java丰富,但是其小而美的设计依然可以满足大部分简单的场景,如下所示。

权限描述符

Java的权限描述符有private,public,protected,default

Golang中则是,如变量,结构体,方法等首字母大写,则其他包都可以访问,首字母小写,则不可访问

2.go几个比较重要的点

(1)select语句

  • 每个 case 都必须是一个通道
  • 所有 channel 表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果任意某个通道可以进行,它就执行,其他被忽略。
  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
    否则:
    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。

(2)高级特性

  • context.WithCancel、context.WithTimeout可以通过上下文通知其他线程统一进行取消或者进行超时释放。
  • sync.Mutex 提供互斥锁,还有RWMutex读写锁
  • 使用waitGroup+chan+context.WithCancel实现多线程的通信,模拟某一线程报错时,用可取消上下文来终止所有线程的运行
Go 复制代码
func MyWork(ctx context.Context, flag int, wg *sync.WaitGroup, erCh chan error) {

	defer wg.Done()
	for {
		select {
		case _ = <-ctx.Done():
			fmt.Printf("当前线程优雅退出%d\n", flag)
			return
		default:
			if time.Now().UnixNano()%5 == 0 {
				erCh <- errors.New("error happened")
				fmt.Printf("当前线程自行退出%d\n", flag)
				return
			}
			fmt.Println("当前线程正在执行任务{}", flag)
			time.Sleep(1 * time.Second)
		}

	}

}
func main(){
		var erCh chan error
		erCh = make(chan error, 3)
		var wg = &sync.WaitGroup{}
		ctx, cancel := context.WithCancel(context.Background())

		go func() {
			if err := <-erCh; err != nil {
				fmt.Println("发生错误")
				cancel()
			}
		}()
		for i := 0; i < 3; i++ {
			wg.Add(1)
			go sync2.MyWork(ctx, i, wg, erCh)
		}

		wg.Wait()
		close(erCh)
		fmt.Println("所有协程优雅退出")
}

(3)go语言中的参数传递(值传递和指针传递)

Go 复制代码
	var once sync.Once
	var wg *sync.WaitGroup
	wg = &sync.WaitGroup{}
	onceBody := func() {
		fmt.Println("Only execute once")
	}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			once.Do(onceBody)
		}()
	}
	wg.Wait()
Go 复制代码
	var once sync.Once
	var wg sync.WaitGroup
	onceBody := func() {
		fmt.Println("Only execute once")
	}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			once.Do(onceBody)
		}()
	}
	wg.Wait()

注意看,两段代码执行都能成功,一个使用了waitGroup的引用,一个是值

但是在多线程中,都能操作统一个waitGroup,这是为什么呢?按理说,值传递的话,wg进行扣除的时候,应该是使用的拷贝副本信息,但依然执行正确

事实上,匿名函数闭包直接引用外部的 wg,Go 会自动处理为指针操作,等效于

Go 复制代码
go func(wgPtr *sync.WaitGroup) {
    defer wgPtr.Done()
}(&wg)

go自动将值传递变成了引用传递

这样的情况也发生在显式参数传递上,例如下面的例子

Go 复制代码
type Person struct {
	age int
}

func (p Person) howOld() int {
	return p.age
}

func (p *Person) growUp() {
	p.age += 1
}

func main() {
	// qcrao 是值类型
	qcrao := Person{age: 18}

	// 值类型 调用接收者也是值类型的方法
	fmt.Println(qcrao.howOld())

	// 值类型 调用接收者是指针类型的方法
	qcrao.growUp()
	fmt.Println(qcrao.howOld())

	// ----------------------

	// stefno 是指针类型
	stefno := &Person{age: 100}

	// 指针类型 调用接收者是值类型的方法
	fmt.Println(stefno.howOld())

	// 指针类型 调用接收者也是指针类型的方法
	stefno.growUp()
	fmt.Println(stefno.howOld())
}

总结就是下面,对于可引用的类型,在转化时可升级成指针

- 值接收者 指针接收者
值类型调用者 方法会使用调用者的一个副本,类似于"传值" 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
指针类型调用者 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() 实际上也是"传值",方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

(4)slice切片

golang里使用slice可变数组来模拟List的效果,底层原理依然是使用数组来实现,slice底层数据结构维护三个字段:引用的数组、slice长度、slice容量,可以通过len()和cap()查切片的长度和容量,可以参考下面示例来理解slice的工作原理

Go 复制代码
	i1 := [5]int{1, 2}
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]
	s2 := s1[2:6:7]

	s2 = append(s2, 100)
	s2 = append(s2, 200)
	s1[2] = 20

	fmt.Println(i1)
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(slice)

-对于数组i1的打印,直接打印[1 2 0 0 0],对于未赋初值的index,默认是0

-对于slice切片,初始化为[0 1 2 3 4 5 6 7 8 9],len=10,cap=10

-s1切片,从slice的下标2开始截取,截取长度为5-2=3,s1容量未明确说明,默认使用指向的数组的长度10,所以s1的容量cap为10-2=8,此时如果print的话,应该为[2,3,4],但其底层指向的数组实际上是[2,3,4,5,6,7,8,9],但并不会被打印出来

-s2切片,从s1的下标2开始截取,截取长度为6-2=4,容量为7-2=5,此时如果print的话,应该为[4,5,6,7],实际底层指向的数组为[4,5,6,7,8],但并不会被打印出来

-对切片s2进行append100后,容量够用,变为[4,5,6,7,100],len=5,cap=5,由于s1,s2,slice都指向同一个底层数组的指针,所以会同步修改slice=[0,1,2,3,4,5,6,7,100,9] s1=[2,3,4,5,6,7,100,9]

-对切片s2再进行append200后,容量不够用,需要扩容,会将原数组复制一份,指向新的数组s2=[4,5,6,7,100,200,-,-,-,-],len=6,cap=10,此时,s2不再与s1和slice的底层数组指向同一处。

-s1[2]=20,会同步修改s1和slice,所以s1=[2,3,20,5,6,7,100,9],slice=[0,1,2,3,20,5,6,7,100,9]

运行结果如下,证明猜想正确

(5)for+range遍历

go语言提供了更加全面的遍历方法,使用for+range来遍历数组、slice、map、chan,来获取不同的值,对于map,返回key和value;对于数组和slice,返回index和value;对于chan,返回value 。当然,其实Java借助Lamda表达式,可以更简单的实现所有对象类型的遍历。

Go 复制代码
	var map1 map[string]string
	map1 = make(map[string]string)
	map1["mellowChen"] = "mellowChen"
	map1["yiguangshen"] = "yiguangshen"

	var chan1 chan string
	chan1 = make(chan string, 1)
	chan1 <- "mellowchen"
	close(chan1)

	for index, value := range i1 {
		print(index, value)
	}

	for index, value := range slice {
		print(index, value)
	}

	for key, value := range map1 {
		print(key, value)
	}

	for value := range chan1 {
		print(value)
	}
相关推荐
present--0110 分钟前
【JAVA EE初阶】多线程(进阶)
java·java-ee
小猪咪piggy12 分钟前
【JavaEE】(10) JavaEE 简介
java·spring·java-ee
yangmf204017 分钟前
Easysearch 冷热架构实战
java·大数据·elasticsearch·搜索引擎
花酒锄作田18 分钟前
[Python][Go]比较两个JSON文件之间的差异
python·golang
麦兜*30 分钟前
LangChain4j终极指南:Spring Boot构建企业级Agent框架
java·spring boot·spring·spring cloud·ai·langchain·ai编程
weixin_3077791338 分钟前
C++实现MATLAB矩阵计算程序
开发语言·c++·算法·matlab·矩阵
捏尼卜波卜1 小时前
try/catch/throw 简明指南
linux·开发语言·c++
_poplar_1 小时前
09 【C++ 初阶】C/C++内存管理
c语言·开发语言·数据结构·c++·git·算法·stl
IDOlaoluo1 小时前
Linux 安装 JDK 8u291 教程(jdk-8u291-linux-x64.tar.gz 解压配置详细步骤)
java·linux·运维
都叫我大帅哥2 小时前
深入解剖Shenandoah GC:Java低延迟垃圾回收的终极武器
java·jvm