Java选手如何看待Golang
写在前面:翻了很多博客,一直没有Java选手转行golang的学习经验贴,思考很久,写下这篇Java选手怎么看待golang这个冉冉新星。
1.走完所有golang基础之后的感受
(1)最大的不适应有这么几点:
- ---变量定义时,变量类型放在后面,如var temp string;但是Java是在前面,比如int a;
- ---新建对象时,在go中常用下面这么几种,make()、new()、&引用传递
在新建map、chan、slice时,我们常用make,可以快速创建,并且赋空值,而new和&常用来新建结构体或者接口时使用,可以快速返回创建的对象的引用。
//对象创建var temp1 chan stringtemp1 = make(chan string, 2) //带缓冲区var temp2 []inttemp2 = make([]int, 10)var temp3 map[int]stringtemp3 = make(map[int]string)var temp4 *dto.Womantemp4 = &dto.Woman{Age1: 0,Name1: "",}temp4.Name("嘿嘿嘿")var temp5 *dto.Mentemp5 = new(dto.Men)temp5.Name("嘎嘎嘎")temp1 <- "12"temp2[0] = 1temp3[12] = "12"
- ---接口方面,没有严格的implements接口,只是可以新定义struct,然后实现这个接口的所有方法,相当于间接实现了这个接口。
var temp4 dto.Mantemp4 = &dto.Woman{Age1: 0,Name1: "",}
tips:
Go 的接口变量本身是一个 双字结构(two-word structure),包含:
- 动态类型信息(指向具体类型的元数据)
- 动态值(指向实际数据的指针或值)
声明接口类型时,无需声明为指针类型,事实上,接口类型可以指向任何类型,本身接口就可以是一个指针类型
(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¬ify,await¬ifyALL,AtomicInteger在内的各种自增原子类,不得不说,Java的生态真的好,各种工具类用到你手软,而且是内置到JDK中!!!无需你自己去挑第三方包
而且从JDK21后开始全面支持虚拟线程,使用也非常简单,API方法完全一样,只是创建的时候加上ofVirtual,下面给出实例
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 会随机公平地选出一个执行,其他不会执行。
否则:- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
(2)高级特性
- context.WithCancel、context.WithTimeout可以通过上下文通知其他线程统一进行取消或者进行超时释放。
- sync.Mutex 提供互斥锁,还有RWMutex读写锁
- 使用waitGroup+chan+context.WithCancel实现多线程的通信,模拟某一线程报错时,用可取消上下文来终止所有线程的运行
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)returndefault: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 errorerCh = 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语言中的参数传递(值传递和指针传递)
var once sync.Oncevar wg *sync.WaitGroupwg = &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()
var once sync.Oncevar wg sync.WaitGrouponceBody := 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 func(wgPtr *sync.WaitGroup) {defer wgPtr.Done()
}(&wg)
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的工作原理
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] = 20fmt.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表达式,可以更简单的实现所有对象类型的遍历。
var map1 map[string]stringmap1 = make(map[string]string)map1["mellowChen"] = "mellowChen"map1["yiguangshen"] = "yiguangshen"var chan1 chan stringchan1 = 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)}