Bütün yazılara qayıt
MƏQALƏ

Java proqramçıları üçün Go

Java bilən proqramçılar üçün praktik Go bələdçisi: yan-yana kod müqayisələri, pointer və interfeyslər, xəta emalı və real backend nümunələri.

35 dəq. oxu
-
Java və Go kodlarının yan-yana müqayisəsi, Java loqosu və Go maskotu.

Go öyrənməyə necə başlamaq olar?

Java ilə servis yazmısansa, Go öyrənərkən biznes qaydalarını, HTTP-ni və verilənlər bazasını yenidən öyrənmirsən. Əsas iş tanış problemlərin başqa dildə necə ifadə olunduğunu anlamaqdır: məlumat harada saxlanır, kim dəyişə bilər, xəta necə ötürülür və paralel əməliyyatlar necə idarə olunur?

Bu bələdçi həmin suallar ətrafında qurulub. Java nümunəsini oxu, Go qarşılığı ilə müqayisə et, sonra əsas davranış fərqinə bax. Məqsəd Java kodunu sətir-sətir tərcümə etmək deyil; Go-da oxunaqlı və etibarlı backend kodu yazmaqdır.

Oxu xəritəsi: əvvəl sintaksis və kolleksiyalar, sonra dəyər və pointer semantikası, daha sonra interfeyslər və xəta emalı, sonda isə real servis və paralel sorğular.

Nümunələrin çoxu bir anlayışı göstərən kod parçalarıdır. Lazım olan import və köməkçi tiplər hər parçada təkrarlanmır. Son bölmədə ayrıca işə sala biləcəyin tam proqram var. Java nümunələri Java 21+, Go nümunələri Go 1.22+ sintaksisinə uyğundur.

Class modelindən tip və davranış modelinə

Java-da məlumatı və metodları çox vaxt bir class daxilində birləşdirirsən. Go-da struct sahələri müəyyənləşdirir, metod isə ayrıca elan edilərək tipə bağlanır. Go-da class irsiyyəti və abstract class yoxdur; mövcud davranışdan istifadə etmək üçün kompozisiya və kiçik interfeyslər qurulur.

Bu fərq Java-nın mütləq mürəkkəb, Go-nun isə hər vəziyyətdə sadə olması demək deyil. Hər iki dildə sadə dizayn mümkündür. Go-da sadəcə dilin verdiyi vasitələr və idiomatik seçimlər fərqlidir.

Eyni model, fərqli təşkilat
Java
final class User {
    private final String name;

    User(String name) {
        this.name = name;
    }

    String greeting() {
        return "Hello " + name;
    }
}
Go
type User struct {
    Name string
}

func (u User) Greeting() string {
    return "Hello " + u.Name
}

// Funksiya daxilində:
user := User{Name: "Aysel"}
message := user.Greeting()
Davranış fərqi

Go-da (u User) metodun receiver-idir: metodun hansı tipə aid olduğunu göstərir. u adi parametr kimi addır, xüsusi this açar sözü deyil. Metodlar yalnız struct-lara deyil, müəyyən şərtlərlə paketdə elan edilmiş digər adlı tiplərə də bağlana bilər.

İlk proqram, dəyişənlər və görünürlük

İcra olunan Go proqramının giriş nöqtəsi package main daxilindəki func main() funksiyasıdır. func funksiyanı elan edir, nəticənin tipi isə parametr siyahısından sonra yazılır. Sətir sonlarında nöqtəli vergül adətən avtomatik əlavə olunur; for başlığında isə onu açıq şəkildə yazırsan.

:= qısa dəyişən elanıdır və yalnız funksiya daxilində işləyir. Eyni əhatə dairəsində ən azı bir yeni, _ olmayan dəyişən tələb edir. Mövcud dəyişəni yeniləmək üçün = istifadə olunur.

İşə salına bilən giriş nöqtəsi
Java
public class Main {
    public static void main(String[] args) {
        var name = "Aysel";
        System.out.println("Hello " + name);
    }
}
Go
package main

import "fmt"

func main() {
    name := "Aysel"
    fmt.Println("Hello " + name)
}
Davranış fərqi

Go-da istifadə olunmayan lokal dəyişən və import kompilyasiya xətasıdır. gofmt isə girintiləri və standart formatı avtomatik tənzimləyir.

Görünürlük class deyil, paket sərhədinə bağlıdır
Java
public class Account {
    public long id;
    private String secret;
}
Go
package account

type Account struct {
    ID     int64
    secret string
}

var applicationName = "Proxy Academy"
const MaxRetry = 5
Davranış fərqi

Böyük hərflə başlayan Account, ID və MaxRetry eksport olunur. secret eyni paketdəki digər fayllardan da görünür. Buna görə onu Java-dakı class səviyyəli private ilə tam eyniləşdirmək olmaz. const kompilyasiya zamanı sabit olan dəyərlər üçündür; Java-dakı hər final sahənin qarşılığı deyil.

Ədədi tiplər, string və zero value

Adların oxşarlığı ölçülərin və davranışın eyni olduğunu göstərmir. Xüsusilə byte, int və mətn üzərində işləyəndə bu fərqlərə diqqət et.

JavaGo-da yaxın seçimVacib fərq
byteint8Java byte işarəlidir. Go byte isə uint8 üçün alias-dır, 0-255 aralığındadır.
shortint16Hər ikisi 16 bitlik işarəli tam ədəddir.
intint32Java int həmişə 32 bitdir. Go int platformadan asılı olaraq 32 və ya 64 bitdir.
longint64ID kimi ölçüsü sabit sahələr üçün uyğundur.
float, doublefloat32, float64İkilik üzən nöqtəli ədədlərdir; pul üçün dəqiqlik siyasəti ayrıca seçilməlidir.
booleanbooltrue və false.
charruneJava char UTF-16 kod vahididir. Go rune, int32 alias-ı olaraq Unicode kod nöqtəsini ifadə edir.
StringstringGo string dəyişməyən bayt ardıcıllığıdır; len simvol deyil, bayt sayını qaytarır.

len("Go") 2, len("ə") isə UTF-8-də 2 qaytarır. for range mətn üzərində kod nöqtələri ilə işləyir; ekranda görünən bir simvol yenə bir neçə kod nöqtəsindən ibarət ola bilər.

Go-da başlanğıc dəyəri verilməyən dəyişən zero value alır: ədədlər 0, bool false, string "", pointer, slice, map, channel və interface isə nil. Java-da sahələr üçün default dəyər var, amma lokal dəyişən oxunmazdan əvvəl mənimsədilməlidir.

Go
var count int          // 0
var active bool        // false
var name string        // ""
var user *User         // nil
var names []string     // nil; append işləyir
var scores map[string]int // nil; oxuma mümkündür

names = append(names, "Aysel")
scores = make(map[string]int) // yazmadan əvvəl yarat
scores["Aysel"] = 95

nil map-ə yazmaq panic-ə səbəb olur. Zero value-nun mövcud olması hər əməliyyatın həmin dəyərlə təhlükəsiz olması demək deyil.

Array, slice və map: oxşarlığın sərhədləri

Go-da array-in uzunluğu tipin bir hissəsidir: [3]int və [5]int fərqli tiplərdir. Array dəyəri mənimsədiləndə elementləri kopyalanır. Dəyişən uzunluqlu ardıcıllıq üçün adətən []T, yəni slice istifadə edilir.

Slice Java List-i ilə eyni obyekt modeli deyil. O, altdakı massivə baxışı, uzunluğu və tutumu saxlayan kiçik dəyərdir. Slice kopyası həmin massivə istinadı paylaşa bilər. append tutum çatmayanda yeni massiv ayıra bildiyi üçün qaytardığı nəticəni mütləq saxla.

Ardıcıllığa element əlavə etmək
Java
List<String> names = new ArrayList<>();
names.add("Ali");
names.add("Aysel");

String first = names.get(0);
int size = names.size();
Go
names := []string{"Ali"}
names = append(names, "Aysel")

first := names[0]
size := len(names)

copyOfNames := make([]string, len(names))
copy(copyOfNames, names)
Davranış fərqi

copy burada ayrıca massivdə string elementləri yaradır. Elementlər pointer, map və ya slice olsa, bu əməliyyat onların istinad etdiyi məlumatı dərindən kopyalamaz. Sərhəddən kənar indekslə oxuma panic yaradır.

Açar yoxdur, yoxsa dəyər sıfırdır?
Java
Map<String, Integer> scores = new HashMap<>();
scores.put("Ali", 0);

if (scores.containsKey("Ali")) {
    System.out.println(scores.get("Ali"));
}
Go
scores := map[string]int{"Ali": 0}

score, exists := scores["Ali"]
if exists {
    fmt.Println(score)
}

delete(scores, "Ali")
Davranış fərqi

Tək scores["Ali"] oxunuşu olmayan açar üçün də 0 qaytarır. value, ok forması bu iki halı ayırır. Map-in iterasiya sırası zəmanətli deyil; paralel oxuma və yazma ayrıca sinxronlaşdırılmalıdır.

Şərtlər, dövrlər və funksiyalar

Go-da while yoxdur: sayğaclı dövr, şərtli dövr və sonsuz dövr üçün for işlədilir. range slice üzərində indeks və element qaytarır; _ istifadə etmədiyin nəticəni buraxır. Alınan element bir dəyər kopyasıdır, ona görə struct elementini dəyişmək istəyəndə çox vaxt items[i] ilə işləmək lazımdır.

if şərtini mötərizəyə almaq məcburi deyil, mötərizədən istifadə etmək də qadağan deyil. switch-in uyğun case-i avtomatik növbəti case-ə keçmir. Yalnız açıq fallthrough yazıldıqda növbəti case-in gövdəsinə keçid olur.

Dövr və anonim funksiya
Java
for (String name : names) {
    System.out.println(name);
}

Runnable greet = () -> {
    System.out.println("Hello");
};
greet.run();
Go
for _, name := range names {
    fmt.Println(name)
}

greet := func() {
    fmt.Println("Hello")
}
greet()
Davranış fərqi

Go funksiyaları dəyişənlərə mənimsədilə və başqa funksiyalara ötürülə bilər. func() { ... }() anonim funksiyanı çağırır; önünə go yazmaq onu ayrıca goroutine-də başladır.

Bir neçə nəticə qaytarmaq
Java
static int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException(
            "division by zero");
    }
    return a / b;
}
Go
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
Davranış fərqi

Go-da nəticə ilə xəta eyni imzada görünür. Bu funksiya tam ədəd bölməsi aparır. Java-da da bir neçə dəyəri record ilə qaytarmaq mümkündür; multiple return Go-da dilin birbaşa dəstəklədiyi formadır. Go metod overloading dəstəkləmədiyi üçün FindByID və FindByEmail kimi ayrı adlar seçilir.

Dəyər, ünvan və pointer: nə kopyalanır?

Həm Java, həm də Go arqumentləri dəyər üzrə ötürür. Java-da obyektə istinadın dəyəri kopyalanır. Go-da isə parametr User olduqda struct, *User olduqda pointer dəyəri kopyalanır. Pointer vasitəsilə eyni struct-a çatdığın üçün onun sahəsini dəyişə bilirsən.

İfadəMənası
user := User{}User dəyəri yaradır.
&useruser dəyişəninin ünvanını alır, tipi *User olur.
var p *UserUser-a pointer elan edir; başlanğıcda nil-dir.
*pPointer-in göstərdiyi dəyərə çıxış verir; p == nil olduqda panic yaranır.
p.NameStruct pointer-ində sahəyə rahat çıxışdır, (*p).Name kimi işləyir.

Struct kopyası pointer və slice kimi sahələrin istinad etdiyi məlumatı dərindən kopyalamır. Pointer-dən istifadə avtomatik performans qazancı vermir: yaddaşda yerləşdirmə və ayrılma xərclərini kompilyatorun escape analysis nəticəsi və ölçmələr müəyyən edir.

Eyni obyektin sahəsini dəyişmək
Java
static void rename(User user) {
    user.name = "Murad";
}

// user.name sahəsi dəyişə bilir.
// user parametrinə başqa obyekt vermək isə
// çağıranın istinadını yeniləmir.
Go
func renameCopy(user User) {
    user.Name = "Murad"
}

func renameOriginal(user *User) {
    user.Name = "Murad"
}

// Funksiya daxilində:
u := User{Name: "Ali"}
renameCopy(u)       // u.Name: Ali
renameOriginal(&u)  // u.Name: Murad
Davranış fərqi

Go-da funksiya imzası paylaşma niyyətini göstərir. Kiçik, müstəqil dəyər üçün User, eyni model üzərində dəyişiklik üçün *User münasib ola bilər. Pointer-in qəbul edildiyi API-də nil-ə icazə verilib-verilmədiyini də müəyyən et.

Receiver, konstruktor və invariantlar

Dəyişiklik edən metod üçün pointer receiver istifadə et. Kiçik və təhlükəsiz kopyalanan dəyərlərdə oxuma metodu value receiver ola bilər. Tərkibində sync.Mutex olan tip kimi kopyalanmamalı strukturlarda value receiver uyğun deyil. Eyni tipdə receiver seçimini ardıcıl saxlamaq API-ni anlamağı asanlaşdırır.

NewUser dilin xüsusi konstruktoru deyil, adi funksiyadır. Lazım olduqda yoxlama aparır və (value, error) qaytarır. new(User) sıfır dəyərli User üçün pointer verir; make isə slice, map və channel yaratmaq üçündür. Bu anlayışları bir-biri ilə qarışdırma.

Ad boş qala bilməz
Java
final class User {
    private String name;

    User(String name) {
        rename(name);
    }

    void rename(String name) {
        if (name.isBlank()) {
            throw new IllegalArgumentException(
                "name is required");
        }
        this.name = name;
    }
}
Go
type User struct {
    name string
}

func NewUser(name string) (*User, error) {
    u := &User{}
    if err := u.Rename(name); err != nil {
        return nil, err
    }
    return u, nil
}

func (u *User) Rename(name string) error {
    if strings.TrimSpace(name) == "" {
        return errors.New("name is required")
    }
    u.name = name
    return nil
}
Davranış fərqi

Eksport olunmayan sahə başqa paketlərin onu birbaşa dəyişməsinin qarşısını alır. Ancaq eksport edilmiş User tipinin zero value-su hələ yaradıla bilər. NewUser funksiyasının olması təkbaşına həmin invariantı dil səviyyəsində məcburi etmir; zero value davranışını API dizaynında nəzərə al.

Interfeys davranışı təsvir edir

Go-da implements yazılmır. Tip tələb olunan metodlar toplusunu təmin edirsə, interfeysə uyğundur. Bu yanaşma çağıran tərəfə yalnız ehtiyacı olan davranışı təsvir etməyə imkan verir. Hər struct üçün əvvəlcədən interfeys yaratmaq lazım deyil.

Kiçik istehlakçı interfeysi
Java
interface UserFinder {
    User findById(long id);
}

final class MemoryUsers implements UserFinder {
    @Override
    public User findById(long id) {
        return new User(id, "Aysel");
    }
}
Go
type UserFinder interface {
    FindByID(id int64) (User, error)
}

type MemoryUsers struct{}

func (m *MemoryUsers) FindByID(
    id int64,
) (User, error) {
    return User{ID: id, Name: "Aysel"}, nil
}

var _ UserFinder = (*MemoryUsers)(nil)
Davranış fərqi

Son sətir kompilyasiya zamanı uyğunluq yoxlamasıdır. Burada pointer receiver olduğuna görə interfeysi *MemoryUsers təmin edir, MemoryUsers dəyəri yox. Value receiver metodları isə həm T, həm də *T metod toplusuna daxil olur.

Kompozisiya və embedding irsiyyət deyil

Go-da başqa tipi sahə kimi saxlamaq kompozisiyadır. Tipi ayrıca sahə adı vermədən yerləşdirmək embedding adlanır; sahə və metodlara daha qısa yolla müraciət etməyə imkan verir. Bu, alt sinif və dinamik override modeli yaratmır.

Məlumatı birləşdirmək
Java
record User(String name) {}
record AdminUser(
    User user,
    List<String> permissions
) {}

// Metod daxilində:
var admin = new AdminUser(
    new User("Aysel"), List.of("read"));
String name = admin.user().name();
Go
type User struct { Name string }

type AdminUser struct {
    User
    Permissions []string
}

// Funksiya daxilində:
admin := AdminUser{
    User: User{Name: "Aysel"},
    Permissions: []string{"read"},
}
name := admin.Name
Davranış fərqi

admin.Name, admin.User.Name üçün qısa seçimdir. AdminUser dəyərini User gözləyən funksiyaya birbaşa ötürə bilməzsən, admin.User verməlisən. İki embedded tipdə eyni adlı üzv olduqda seçim qeyri-müəyyən ola bilər.

Xətanı dəyər kimi qaytar və səbəbini qoru

Go-da gözlənilən uğursuzluqlar üçün adi seçim error qaytarmaqdır. Çağıran kod onu emal edir, əlavə kontekstlə yuxarı ötürür və ya cavaba çevirir. panic və recover var, amma onlar gündəlik biznes xətaları üçün try/catch əvəzi kimi istifadə edilməməlidir.

fmt.Errorf daxilində %w səbəbi qoruyaraq xətanı bükür. Sonra errors.Is ilə həmin səbəbi, errors.As ilə uyğun xəta tipini yoxlaya bilərsən. Xəta mətnini müqayisə etmək format dəyişikliyinə həssasdır.

Səbəbi itirmədən ötürmək
Java
try {
    return repository.findById(id);
} catch (SQLException cause) {
    throw new UserReadException(
        "read user " + id, cause);
}
Go
user, err := repository.FindByID(ctx, id)
if err != nil {
    return User{}, fmt.Errorf(
        "read user %d: %w", id, err,
    )
}
return user, nil
Davranış fərqi

Xəta mətninə diaqnostika üçün əməliyyat və təhlükəsiz identifikator əlavə et. Parol, token və şəxsi məlumat yazma. HTTP sərhədində daxili xəta mətnini istifadəçiyə birbaşa göstərmək əvəzinə uyğun status və ümumi mesaj qaytar.

nil interface tələsi

Go interface dəyərini anlamaq üçün dinamik tip və dinamik dəyəri birlikdə düşün. Hər ikisi olmadıqda interface nil olur. İçində nil pointer olan interface isə həmin pointer-in tipini daşıdığı üçün nil deyil.

Go
type ReadError struct{}
func (*ReadError) Error() string { return "read failed" }

var pointer *ReadError = nil
var err error = pointer

fmt.Println(pointer == nil) // true
fmt.Println(err == nil)     // false

Uğurlu nəticə üçün error qaytarırsansa, konkret tipli nil pointer-i deyil, birbaşa nil qaytar. Bu fərq mock repository və şərti xəta yaratma kodunda xüsusilə asan gözdən qaçır.

defer: resursun ömrünü funksiyaya bağla

defer çağırışı funksiya başa çatarkən icra olunur. Bir neçə defer əks qeydiyyat sırası ilə işləyir. Arqumentlər defer elan ediləndə hesablanır. Dövr daxilindəki defer isə iterasiyanın yox, bütöv funksiyanın sonunu gözləyir.

Oxuma üçün açılan faylın bağlanması
Java
try (var input = Files.newInputStream(path)) {
    byte[] data = input.readAllBytes();
    return data;
}
Go
func readFile(path string) ([]byte, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    return io.ReadAll(file)
}
Davranış fərqi

Defer resurs uğurla açılandan sonra yazılır. Bu nümunə oxuma üçündür və Close xətasını yoxlamır. Fayla yazma zamanı Close və Flush xətaları məlumatın saxlanmasına təsir edə bilər, onları ayrıca emal etmək lazımdır. SQL-də çoxsətirli sorğudan sonra rows.Close() və iterasiyadan sonra rows.Err() də yoxlanmalıdır.

Dependency injection: əlaqələri açıq qur

Dependency injection üçün konteyner şərt deyil. Go-da asılılıqları tətbiqin başlanğıcında yaradıb konstruktor funksiyalarına ötürmək geniş istifadə olunan yanaşmadır. Bu, testdə repository-ni saxta implementasiya ilə əvəz etməyə də imkan verir.

Java-da da eyni əl ilə qurulma mümkündür. Spring əlavə olaraq bean lifecycle və avtomatik əlaqələndirmə kimi imkanlar verir; Go-da aşağıdakı nümunə həmin konteyner funksiyalarını təkrarlamır.

Servisin asılılığını konstruktorla vermək
Java
final class UserService {
    private final UserRepository repository;

    UserService(UserRepository repository) {
        this.repository = repository;
    }
}

// Tətbiqin qurulması:
var repository = new PostgresUsers(dataSource);
var service = new UserService(repository);
Go
type UserService struct {
    repository UserRepository
}

func NewUserService(r UserRepository) *UserService {
    return &UserService{repository: r}
}

// Tətbiqin qurulması:
repository := NewPostgresUsers(db)
service := NewUserService(repository)
Davranış fərqi

Konstruktor imzası servisin nəyə ehtiyac duyduğunu göstərir. Asılılıqları qlobal dəyişənlərdə və ya context daxilində gizlətmək əvəzinə açıq şəkildə ötür. Kiçik layihədə hər qat üçün ayrıca abstraksiya yaratmaq məcburi deyil.

Context: sorğu ləğvini bütün qatlara çatdır

context.Context ləğv siqnalını, deadline-ı və sorğuya aid kiçik metadatanı daşıyır. HTTP handler-də r.Context() ilə başlayıb service və repository qatlarına ötür. Repository-də yenidən context.Background() yaratmaq gələn sorğunun ləğv əlaqəsini qırar.

Go
func (s *UserService) FindByID(
    ctx context.Context, id int64,
) (User, error) {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    return s.repository.FindByID(ctx, id)
}

Əgər üst context-in deadline-ı daha tezdirsə, yeni context onu uzatmır. defer cancel() ayrılmış resursların iş bitən kimi azad edilməsinə kömək edir.

Context goroutine-i zorla dayandırmır. Çağırılan əməliyyat ctx.Done() siqnalını və ya context qəbul edən API-ni istifadə etməlidir. Sonsuz hesablama dövrü context-i yoxlamırsa, timeout təkbaşına onu bitirməyəcək. Context-i ixtiyari parametrlər üçün konteynerə çevirmə və adi dependency-ləri onun içinə yığma.

Real repository: sorğu, xəta və pointer bir yerdə

Aşağıdakı repository PostgreSQL placeholder sintaksisindən ($1) istifadə edir. *sql.DB bir bağlantı deyil, paylaşılabilən bağlantı hovuzudur. Driver-in qeydiyyatı, bağlantının yaradılması və pool limitləri tətbiqin başlanğıcında qurulur.

QueryRowContext sorğu nəticəsini Scan zamanı oxuyur. Uyğun sətir yoxdursa sql.ErrNoRows qaytarılır. Onu domen səviyyəsində mənalı xətaya çevirmək service qatını SQL detalından ayırır.

Go
var ErrUserNotFound = errors.New("user not found")

type User struct {
    ID   int64
    Name string
}

type UserRepository interface {
    FindByID(context.Context, int64) (User, error)
}

type PostgresUsers struct {
    db *sql.DB
}

func NewPostgresUsers(db *sql.DB) *PostgresUsers {
    return &PostgresUsers{db: db}
}

func (r *PostgresUsers) FindByID(
    ctx context.Context, id int64,
) (User, error) {
    var user User
    err := r.db.QueryRowContext(ctx,
        `SELECT id, name FROM users WHERE id = $1`,
        id,
    ).Scan(&user.ID, &user.Name)

    if errors.Is(err, sql.ErrNoRows) {
        return User{}, ErrUserNotFound
    }
    if err != nil {
        return User{}, fmt.Errorf("query user: %w", err)
    }
    return user, nil
}

Scan(&user.ID, &user.Name) sahələrin ünvanını verir ki, driver nəticəni onların içinə yaza bilsin. Sorğu parametri SQL mətninə birləşdirilmir, ayrıca ötürülür. Bu nümunədə name sütununun NOT NULL olduğu fərz edilir; nullable sütun üçün sql.NullString kimi uyğun tip seçilməlidir.

Qaytarılan model kiçik olduğu üçün burada User dəyəri seçilib. (User, error) ilə uğursuz halda zero value qaytarmaq normaldır: çağıran kod əvvəl xətanı yoxlamalıdır. (*User, error) də mümkündür, amma uğurlu nəticədə nil olub-olmayacağı müqavilədə aydın olmalıdır.

JSON və HTTP sərhədi

Go encoding/json eksport olunan sahələri emal edir. Struct tag sahənin JSON adını və bəzi kodlaşdırma seçimlərini bildirir; Java annotasiyalarının bütün imkanlarını əvəz etmir.

Cavabın müqaviləsi
Java
public record UserResponse(
    @JsonProperty("user_id") long id,
    String name
) {}
Go
type UserResponse struct {
    ID   int64  `json:"user_id"`
    Name string `json:"name"`
}

response := UserResponse{
    ID: user.ID, Name: user.Name,
}
Davranış fərqi

Açıq modeldə ID böyük hərflə başlamalıdır. Təkcə json:"id" tag-i eksport olunmayan sahəni əlçatan etmir. Decode zamanı Decode(&request) pointer alır, çünki nəticəni modelin içinə yazır.

Xətanı HTTP cavabına çevirmək

Handler nəqliyyat qatının qərarlarını verir: giriş yoxlaması, status kodu, cavab formatı. Repository-nin daxili xəta mətnini müştəriyə ötürməməlidir. Aşağıdakı nümunə ID-ni /users?id=42 sorğusundan götürür və əvvəlki UserService-dən istifadə edir.

Go
func userHandler(service *UserService) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            w.Header().Set("Allow", http.MethodGet)
            http.Error(w, "method not allowed", 405)
            return
        }
        id, err := strconv.ParseInt(
            r.URL.Query().Get("id"), 10, 64,
        )
        if err != nil || id <= 0 {
            http.Error(w, "invalid user id", 400)
            return
        }
        user, err := service.FindByID(r.Context(), id)
        switch {
        case errors.Is(err, ErrUserNotFound):
            http.Error(w, "user not found", 404)
            return
        case errors.Is(err, context.DeadlineExceeded):
            http.Error(w, "upstream timeout", 504)
            return
        case err != nil:
            http.Error(w, "internal server error", 500)
            return
        }
        body, err := json.Marshal(UserResponse{
            ID: user.ID, Name: user.Name,
        })
        if err != nil {
            http.Error(w, "encoding failed", 500)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        if _, err := w.Write(body); err != nil {
            log.Printf("write user response: %v", err)
        }
    }
}

JSON əvvəl yaddaşda hazırlanır ki, kodlaşdırma xətası baş verəndə cavab artıq göndərilmiş olmasın. Write xətasından sonra ikinci HTTP cavabı yazmaq mümkün deyil. Real tətbiqdə loglama, istifadəçi icazələri, sorğu ölçüsü limitləri və server timeout-ları da ayrıca qurulur. POST body-si decode edilirsə, maksimum ölçü və biznes qaydaları yoxlanmalı, lazım olduqda naməlum sahələr və əlavə JSON dəyərləri rədd edilməlidir.

Goroutine başlatmaq işin yarısıdır

go work() funksiyanı ayrıca goroutine-də başladır. Goroutine-ləri Go runtime planlaşdırır; hər goroutine üçün ayrıca OS thread yaradılmır. Java virtual thread ilə müqayisə ilkin təsəvvür yarada bilər, amma iki runtime-ın planlaşdırma və bloklanma mexanizmləri eyni deyil.

main qayıdanda proqram digər goroutine-ləri gözləmir. Onları gözləmək üçün sync.WaitGroup istifadə edə bilərsən. O, nəticə və ya xəta daşımır, yalnız işlərin tamamlanmasını əlaqələndirir.

İki işin tamamlanmasını gözləmək
Java
var pool = Executors.newFixedThreadPool(2);
try {
    Future<?> first = pool.submit(() -> work(1));
    Future<?> second = pool.submit(() -> work(2));
    first.get();
    second.get();
} finally {
    pool.shutdown();
}
Go
var wg sync.WaitGroup
for i := 1; i <= 2; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        work(id)
    }(i)
}
wg.Wait()
Davranış fərqi

Add goroutine başlamazdan əvvəl çağırılır. Java Future.get() nəticə və icra xətası da ötürə bilir, Go WaitGroup bunu etmir. Gözləmək paylaşılan dəyişənə paralel yazıları avtomatik təhlükəsiz etmir. Yeni Go versiyalarında WaitGroup.Go da var; burada daha geniş versiya uyğunluğu üçün Add/Done göstərilir.

Channel: nəticənin və sinxronlaşmanın yolu

ch <- value göndərir, <-ch qəbul edir. Buffersiz channel-də göndəriş və qəbul bir-birini gözləyir. Buffer varsa, tutum dolana qədər göndəriş qəbuledicini gözləmədən tamamlana bilər. Channel bir Future deyil: bir neçə dəyər daşıya bilər, amma nəticəni çoxsaylı oxucular üçün saxlamır.

Bir hesablamanın nəticəsini almaq
Java
CompletableFuture<Integer> future =
    CompletableFuture.supplyAsync(() -> 10 + 20);

int result = future.join();
Go
ch := make(chan int, 1)
go func() {
    ch <- 10 + 20
}()

result := <-ch
Davranış fərqi

Burada hər iki kod bir hesablamanın nəticəsini gözləyir. Go buffer-i bir nəticə tutur. Real əməliyyatda nəticə ilə birlikdə error, gözləmədə isə context ləğvi də nəzərə alınmalıdır.

Channel üçün dörd vacib qayda

  • nil channel-də göndəriş və qəbul bloklanır. make(chan T) ilə yarat. select daxilində nil channel həmin case-i söndürmək üçün qəsdən istifadə oluna bilər.
  • Bağlanmış channel-ə yazmaq panic yaradır. Bağlamağa adətən göndərişin bitdiyini bilən tərəf cavabdeh olur; bir neçə göndərən olduqda koordinasiya lazımdır.
  • Bağlanmış channel-dən oxuma mümkündür. Buffer boşaldıqdan sonra zero value gəlir və value, ok := <-ch ifadəsində ok false olur.
  • Hər channel bağlanmalı deyil. close “daha dəyər gəlməyəcək” siqnalıdır. Qəbuledicinin range dövrünü bitirmək üçün lazımdır, yaddaşı azad etmək üçün məcburi əməliyyat deyil.
Go
select {
case result := <-results:
    return result, nil
case <-ctx.Done():
    return Result{}, ctx.Err()
}

Qəbuledici ləğvə görə çıxırsa, göndərənin sonsuz bloklanmayacağını da düşün. Göndərişi context ilə select-ə salmaq və ya dəqiq sayda nəticəyə uyğun buffer seçmək mümkün həllərdir. Buffer sonsuz yükü və nəzarətsiz goroutine sayını həll etmir.

Paylaşılan vəziyyət və data race

İki goroutine eyni yaddaşa sinxronlaşdırılmadan çatırsa və onlardan biri yazırsa, data race yarana bilər. count++ bir atomik əməliyyat deyil. WaitGroup-la sonda gözləmək bu paralel yazıları düzəltmir.

Sayğacı qorumaq
Java
final class Counter {
    private int value;

    synchronized void increment() {
        value++;
    }

    synchronized int value() {
        return value;
    }
}
Go
type Counter struct {
    mu sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}
Davranış fərqi

Oxuma da eyni sinxronlaşdırma qaydasına tabe olmalıdır. Mutex istifadə edildikdən sonra onu daşıyan struct-ı kopyalama. Channel məlumat ötürmək, mutex isə ortaq vəziyyəti qorumaq üçün uyğun ola bilər; birini hər problemdə digərinin yerinə işlətmə.

Üç backend çağırışını nəzarətlə paralelləşdirmək

Profil endpoint-i istifadəçi, sifariş və balans məlumatını müstəqil xidmətlərdən alırsa, çağırışları paralel başlatmaq olar. Müddətlər 100, 150 və 200 ms olduqda ardıcıl gözləmə təxminən 450 ms-dir. İdeal paralel icra 200 ms-ə yaxınlaşa bilər, amma şəbəkə, pool növbələri və planlaşdırma xərcləri də qalır.

Aşağıdakı fraqmentdə getUser, getOrders və getWallet context qəbul edən və (dəyər, error) qaytaran funksiyalardır. User, Order, Wallet və Profile tətbiqin modelləridir. Nümunə üç sabit iş üçün nəzərdə tutulub.

Go
func loadProfile(parent context.Context) (Profile, error) {
    ctx, cancel := context.WithTimeout(parent, 2*time.Second)
    defer cancel()

    var profile Profile
    var wg sync.WaitGroup
    errs := make(chan error, 3)

    run := func(job func() error) {
        wg.Add(1)
        go func() {
            defer wg.Done()
            if err := job(); err != nil {
                errs <- err
                cancel()
            }
        }()
    }
    run(func() error {
        value, err := getUser(ctx)
        if err == nil { profile.User = value }
        return err
    })
    run(func() error {
        value, err := getOrders(ctx)
        if err == nil { profile.Orders = value }
        return err
    })
    run(func() error {
        value, err := getWallet(ctx)
        if err == nil { profile.Wallet = value }
        return err
    })

    wg.Wait()
    close(errs)
    if err, ok := <-errs; ok {
        return Profile{}, err
    }
    if err := ctx.Err(); err != nil {
        return Profile{}, err
    }
    return profile, nil
}

Niyə burada nəticəyə yazmaq təhlükəsizdir? Hər goroutine ayrıca sahəyə yazır, yekun oxuma Wait-dən sonra olur. Eyni map-ə və ya slice-a paralel append etmək bu qaydaya daxil deyil. Hər iş ən çox bir xəta göndərdiyi üçün üçyerlik buffer göndərişin bloklanmasının qarşısını alır. İlk xəta digər işlərə ləğv siqnalı verir.

Bu kodun vacib şərti var: asılı funksiyalar context-ə reaksiya verməlidir. Əks halda Wait timeout-dan sonra da bloklana bilər. Dəyişən sayda iş üçün məhdud worker pool və ya concurrency limiti qur; hər sorğuya minlərlə goroutine başlatmaq bağlantı hovuzunu və downstream servisi yükləyə bilər.

Generics və adlandırılmış sabitlər

Go-da []User tipli slice generic funksiya yazmadan da mövcuddur. Type parametrləri eyni alqoritmi müxtəlif tiplərlə təhlükəsiz işlətmək üçündür. Boş kolleksiya halını imzada nəzərə almaq API-ni daha etibarlı edir.

İlk elementi təhlükəsiz almaq
Java
static <T> Optional<T> first(List<T> items) {
    if (items.isEmpty()) {
        return Optional.empty();
    }
    return Optional.of(items.get(0));
}
Go
func First[T any](items []T) (T, bool) {
    if len(items) == 0 {
        var zero T
        return zero, false
    }
    return items[0], true
}
Davranış fərqi

Java nümunəsi null elementi qəbul etmir; Go isə sıfır və ya nil dəyəri true ilə qaytara bilər. Buradakı bool yalnız elementin mövcudluğunu bildirir. Eyni semantika lazımdırsa, null/nil siyasətini də açıq müəyyən et.

Status sabitləri
Java
enum Status {
    ACTIVE,
    BLOCKED,
    DELETED
}
Go
type Status string

const (
    StatusActive Status = "ACTIVE"
    StatusBlocked Status = "BLOCKED"
    StatusDeleted Status = "DELETED"
)
Davranış fərqi

Go-da bu tip qapalı dəyərlər çoxluğu deyil: Status("UNKNOWN") yaratmaq mümkündür. Xarici giriş ayrıca yoxlanmalıdır. Ədədi sabitlər üçün iota istifadə edilə bilər, amma sıra dəyişmələri saxlanmış API və DB dəyərlərinə təsir edə biləcəyi üçün həmin müqavilələrdə dəyərləri açıq yazmaq daha təhlükəsizdir.

Paketlər və gündəlik iş axını

Kiçik tətbiqi yüzlərlə abstraksiyaya bölmədən başla. Paket sərhədini istifadəçi, ödəniş və sifariş kimi məsuliyyətlərə görə seçmək olar. handler.go, service.go və repository.go eyni paketdə ayrı fayllar ola bilər; fayl yaratmaq özü görünürlük sərhədi yaratmır.

Struktur
myapp/
├── cmd/api/main.go
├── internal/user/
│   ├── handler.go
│   ├── service.go
│   ├── repository.go
│   └── model.go
├── go.mod
└── go.sum

Bu qovluq quruluşu məcburi standart deyil. internal isə sadəcə ad seçimi deyil, Go import qaydası ilə qorunan sərhəddir: içindəki paketləri yalnız onun valideyn qovluğunun altındakı kod import edə bilər.

Gündəlik yoxlamaları layihənin modul qovluğunda işə sal:

Terminal
go fmt ./...
go vet ./...
go test ./...
go test -race ./...

go fmt formatı, go vet şübhəli konstruksiyaları, testlər gözlənilən davranışı yoxlayır. Race detector yalnız icra olunan kod yollarında baş verən data race-ləri tapır; testin keçməsi bütün mümkün paralel ssenarilərin sübutu deyil.

Java-dakı arxitekturanı olduğu kimi köçürmə: əvvəl konkret əməliyyatı yaz, dəyişən davranış və ya test sərhədi yarananda kiçik interfeys çıxar. Bu prinsip Java layihəsində də faydalıdır.

İşə sal: model, yoxlama və paralel icra

Bu nümunəni main.go faylına yazıb go run main.go ilə işlədə bilərsən. İki istifadəçi əvvəl yaradılır və yenilənir, sonra goroutine-lər yalnız oxuyur. Buna görə burada ortaq modelə paralel yazı yoxdur.

Go
package main

import (
    "errors"
    "fmt"
    "strings"
    "sync"
)

type User struct {
    ID int64
    name string
}

func NewUser(id int64, name string) (*User, error) {
    u := &User{ID: id}
    if err := u.Rename(name); err != nil {
        return nil, err
    }
    return u, nil
}

func (u *User) Rename(name string) error {
    name = strings.TrimSpace(name)
    if name == "" {
        return errors.New("name is required")
    }
    u.name = name
    return nil
}

func (u *User) Greeting() string {
    return "Hello " + u.name
}

func main() {
    first, err := NewUser(1, "Ali")
    if err != nil {
        fmt.Println(err)
        return
    }
    second, err := NewUser(2, "Aysel")
    if err != nil {
        fmt.Println(err)
        return
    }
    if err := first.Rename("Murad"); err != nil {
        fmt.Println(err)
        return
    }

    users := []*User{first, second}
    var wg sync.WaitGroup
    for _, user := range users {
        wg.Add(1)
        go func(u *User) {
            defer wg.Done()
            fmt.Println(u.Greeting())
        }(user)
    }
    wg.Wait()
    fmt.Println("Finished")
}

İlk iki sətrin sırası dəyişə bilər, Finished isə hər iki iş tamamlandıqdan sonra çıxır. Nümunəni genişləndirərkən Rename çağırışını goroutine-lərlə eyni anda işlətsən, artıq sinxronlaşdırma tələb olunacaq.

Yaddaş vərəqi və növbəti məşq

Java-da tanış anlayışGo-da istifadə olunan vasitəUnutma
Obyekt modelistruct və metodlarStruct dəyər kimi kopyalanır.
thisReceiver adı, məsələn uValue və pointer receiver fərqlidir.
KonstruktorNewX funksiyasıXüsusi dil mexanizmi deyil.
List<T>[]TSlice altdakı massivi paylaşa bilər.
Map<K,V>map[K]VMövcudluğu value, ok ilə yoxla.
Exceptionerror nəticəsiXətanı yoxla, bükərkən %w istifadə et.
implementsMetod toplusunun uyğunluğuT və *T eyni deyil.
nullnilTipli nil interface nil olmaya bilər.
Thread / virtual threadGoroutineBaşlatmaqla yanaşı tamamlanmasını da idarə et.
FutureNəticə channel-i kimi bir quruluşTam eyni abstraksiya deyil.
CountDownLatchsync.WaitGroupXətaları özü daşımır.
synchronizedsync.MutexOxuma və yazını eyni qayda ilə qoru.
try-with-resourcesdefer ilə cleanupDefer funksiyadan çıxışda işləyir.
Java enumAdlandırılmış tip və sabitlərDəyərlər çoxluğu avtomatik qapalı deyil.

Praktik tapşırıq: yaddaşda saxlayan UserRepository implementasiyası yaz və UserService-ə ötür. Üç halı test et: istifadəçi tapılır, tapılmır, context əvvəlcədən ləğv edilib. Sonra PostgreSQL implementasiyası ilə əvəz et və servisin dəyişmədiyini yoxla.

Öz kodunu bu suallarla nəzərdən keçir: kim hansı məlumatı dəyişir, xəta hansı sərhəddə emal edilir, goroutine nə vaxt bitir, asılılıq hansı imzada görünür? Bu suallara kodun özündən cavab tapmaq Go-da rahat işləməyə başladığını göstərir.