Erhan Yakut Software Developer @Binalyze | Founder @Passwall | Golang Enthusiast | Open Sorcerer

Go ile Veri Kopyalama ve Taşıma Teknikleri

7 min read

Go Programlama Dili



Go programlama dili, modern yazılım geliştirme pratiklerini destekleyen etkileyici özelliklere sahip olmasıyla bilinir. Google tarafından 2009 yılında tanıtılan bu dil, özellikle eşzamanlılık (concurrency) ve sistem programlaması gibi alanlarda ön plana çıkar. Gömülü sistemlerden büyük ölçekli dağıtık sistemlere kadar geniş bir yelpazede kullanılan Go, veri kopyalama ve taşıma teknikleri açısından da geliştiricilere güçlü araçlar sunar. Bu makalede, Go’nun veri kopyalama ve taşıma teknikleri ile bu tekniklerin kullanımlarını ve püf noktalarını ele alacağız. Bu kapsamlı rehber, hem yeni başlayanlar için temel bir giriş sağlarken hem de deneyimli geliştiricilere derinlemesine bilgiler sunacak. Go ile veri yönetiminin inceliklerini keşfederken, dilin sağladığı avantajları maksimize etmenin yollarını öğreneceksiniz.

copy Fonksiyonunun Kullanımı

Go programlama dilinde, copy fonksiyonu dilin yerleşik (builtin) fonksiyonlarından biridir ve özellikle slice’lar arası veri kopyalama işlemlerinde sıklıkla kullanılır. Bu fonksiyon, kaynak slice’tan hedef slice’a veri kopyalamak için tasarlanmıştır ve kullanımı son derece basittir ancak dikkat edilmesi gereken birkaç nokta vardır.

  1. Bir slice’ı diğerine kopyalarken, kopyalama işlemi hedef slice’ın kapasitesine bağlıdır. Eğer hedef yeterli büyükte değilse, slice’ın tamamı kopyalanamayabilir.
  2. copy fonksiyonu, kopyalanan eleman sayısını döndürür, bu da işlemin başarılı olup olmadığını kontrol etmek için kullanılabilir.
  3. İlk parametre hedef (destination), ikinci parametre kaynak (source)’tır. copy(dst, src)
  4. copy fonksiyonu, slice pointer’ları arasında değil, slice’ların temsil ettiği değerler (elemanlar) arasında kopyalama işlemi gerçekleştirir. Bu, iki slice’ın da aynı underlying array’i paylaşmadığı anlamına gelir; yani copy işlemi sonrası, kaynak slice’daki değişiklikler hedef slice’ı etkilemez!

append Fonksiyonunun Kullanımı

Go programlama dilinde, append fonksiyonu, slice’lara dinamik olarak eleman eklemek için kullanılan yerleşik bir fonksiyondur ve şunlara dikkat etmek gerekir:

  1. append ile, bir slice’ın kapasitesi otomatik olarak yönetilir; eğer slice’ın mevcut kapasitesi eklenmek istenen yeni elemanları alacak kadar büyük değilse, Go dilinin çalışma zamanı sistemi otomatik olarak daha büyük bir kapasiteye sahip yeni bir underlying array oluşturur ve mevcut elemanları yeni array’e kopyalar. Bu işlem, programcıların manuel olarak array yeniden boyutlandırma işlemleriyle uğraşmasının önüne geçer ve dilin kullanım kolaylığını artırır.
  2. Otomatik büyütme işleminde slice genellikle mevcut kapasitenin iki katı kadar büyütülür ancak çok büyük sayılarda bu oran farklı hesaplamalara göre yapılır.
  3. append fonksiyonunun kullanımı, slice’lara eleman eklemenin yanı sıra, slice’ları birleştirmek için de kullanılabilir. Bu fonksiyonun geri dönüş değeri, elemanların eklendiği yeni slice’dır. İşte append fonksiyonunun kullanımına dair bir örnek:

Channel ile Veri Taşıma

Go programlama dilinde, channel’lar goroutine’ler arasında veri alışverişi yapmak için kullanılan güçlü araçlardır. Channel’lar üzerinden veri taşıma, Go’nun eşzamanlılık modelinin temel taşlarından biridir ve “Don’t communicate by sharing memory, share memory by communicating” felsefesini benimser. Bu yaklaşım, race condition gibi sorunların önüne geçer ve veri bütünlüğünü korur. Channel kullanımı, verilerin sıralı ve güvenli bir şekilde bir goroutine’den diğerine aktarılmasını sağlar, bu da özellikle birden çok iş parçacığı arasında koordinasyon gerektiren durumlar için idealdir. Channel’lar, belirli bir tip veri taşımak üzere tip güvenliği sağlar ve hem senkron (bloklayıcı) hem de asenkron (non-bloklayıcı) iletişim desenlerini destekler.

Aşağıda, Go’da channel kullanarak veri taşımanın nasıl gerçekleştirilebileceğine dair basit bir örnek verilmiştir. Bu örnek, bir goroutine içerisinden başka bir goroutine’e mesaj göndermeyi ve almayı gösterir:

Channel, sadece veri taşıma için değil, ayrıca senkronizasyon ve sinyal gönderme gibi bir çok farklı amaçla kullanılabilir. Şurada birçok farklı kullanım örneğine değinmiştim. Bununla birlikte channel kullanırken birçok konuya dikkat etmek, manuel olarak süreci yönetmek gerekir: channelda veri kaldı mı, channel kapalı mı, kapasitesi uygun mu, blocking mi… İşte bu tür şeyleri manuel kontrol etmek yerine temelinde yine channel kullanan io.Pipe veri taşımada daha kullanışlı bir araç olarak karşımıza çıkmaktadır. Ancak io.Pipe’e geçmeden önce Go’nun iki önemli araçlarında io.Writer ve io.Reader interface’lerini anlamamız gerekir.

io.Writer ve io.Reader Kullanımı

Go programlama dilinde, io.Writer ve io.Reader interfaceleri, veri yazma ve okuma işlemleri için standart bir arayüz sağlar. Bu interfaceler, Go’nun I/O işlemlerinin temelini oluşturur. io.Writer interface’i, bir veri akışına veri yazmayı sağlayan Write metodunu tanımlar. Bu interface, dosyalara, buffer’lara, ağ bağlantılarına ve daha fazlasına veri yazmak için kullanılabilir. Öte yandan, io.Reader interface’i, bir veri akışından veri okumayı sağlayan Read metodunu tanımlar. Bu interface aracılığıyla dosyalardan, bellek tamponlarından, ağ bağlantılarından ve benzeri kaynaklardan veri okunabilir. Her iki interface de, Go’nun standart kütüphanesinde yer alan çeşitli paketler tarafından geniş bir şekilde kullanılır ve Go’nun polimorfik I/O işlemlerini destekleme yeteneğinin bir göstergesidir. io.Writer ve io.Reader‘ın bu genel arayüzleri sayesinde, Go geliştiricileri farklı veri kaynakları ve hedefleri arasında kolayca geçiş yapabilir ve veri akışlarını esnek bir şekilde yönetebilir.

type Writer interface {
Write(p []byte) (n int, err error)
}

type Reader interface {
Read(p []byte) (n int, err error)
}

io.Reader’da En Çok Yapılan Hata

io.Reader’ı implemente eden bir Read methodunda en sık yapılan hata, Read metodunun her çağrısında tam olarak istenilen miktarda veri okuyacağını varsaymaktır. Ancak, bu metod daha az veri okuyabilir veya io.EOF hatası dönebilir. Bu sorun genellikle şu durumlarda ortaya çıkabilir:

  1. Veri Kaynağında Yeterli Veri Yok: Eğer veri kaynağında sadece 5 byte veri kaldıysa, Read çağrısı yalnızca bu 5 byte’ı okur ve size geri döndürür. Bu durumda, n (okunan byte sayısı) değeri 5 olur.
  2. Ağ Gecikmeleri ve Veri Parçalanması: Ağ üzerinden veri okurken, veri paketleri parçalara ayrılabilir veya gecikmeler yaşanabilir. Bu nedenle, her Read çağrısı farklı miktarda veri alabilir. Bir çağrıda 8 byte beklerken, gerçekte daha az veri okunabilir.
  3. Dosya Sonu (EOF) Durumu: Dosya sonuna ulaşıldığında, kalan veri 8 byte’tan azsa, Read yalnızca kalan veriyi okur ve io.EOF hatası döndürür.

Hatalı okuma örneği:

Doğru okuma örneği

bytes.Buffer Kullanımı

bytes.Buffer genellikle bellekte veri saklamak ve bu veri üzerinde çeşitli okuma/yazma işlemleri yapmak için kullanılır. bytes.Buffer, hem io.Reader hem de io.Writer interface’lerini uygular, bu sayede veriyi arabellekte saklayabilir ve gerektiğinde üzerinde işlemler gerçekleştirebilirsiniz. bytes.Buffer, bellek üzerinde çalıştığı için, genellikle statik veri üzerinde çalışırken veya veri dönüştürme işlemlerinde tercih edilir.

bufio Kullanımı

Go’nun bufio paketi, buffered giriş ve çıkış işlemleri için güçlü araçlar sunar. Bu paket, dosya okuma/yazma işlemleri, ağ üzerinden veri alışverişi ve standart giriş/çıkış işlemleri gibi I/O işlemlerini daha verimli hale getirmek için kullanılır. bufio paketi içindeki Reader ve Writer yapıları, büyük veri bloklarını işlerken veya sık sık okuma ve yazma işlemleri gerçekleştirirken performansı artırır. Tamponlama mekanizması sayesinde, bufio paketi, uygulamaların daha az sistem çağrısı yapmasını sağlayarak I/O işlemlerinin hızını önemli ölçüde iyileştirebilir. Ayrıca, bufio.Scanner yapısı, satır satır veya belirli bir ayırıcıya göre veri okuma işlemlerini kolaylaştırır, bu da özellikle metin tabanlı veri işlerken oldukça yararlıdır.

Aşağıda, bufio paketini kullanarak bir dosyadan satır satır okuma yapılmasını gösteren bir örneği bulabilirsiniz:

io.Pipe’ın Kullanımı

Go’nun io paketi, çeşitli giriş/çıkış işlemleri için güçlü araçlar sunar ve io.Pipe bu araçlardan biridir. io.Pipe bir pipe (boru) oluşturarak, bir yazma işlemi ile bir okuma işlemi arasında veri akışı sağlar. Bu, genellikle farklı goroutine’ler arasında veri aktarımı yaparken kullanılır. Pipe fonksiyonu, bir io.PipeReader ve bir io.PipeWriter döndürür; PipeWriter üzerine yazılan her şey, PipeReader tarafından okunabilir hale gelir. Bu yapı, özellikle stream edilen veriler veya büyük veri setlerinin işlenmesinde oldukça yararlıdır çünkü verilerin tamamının belleğe alınmasını gerektirmez. Ancak, io.Pipe kullanırken dikkat edilmesi gereken bazı püf noktaları vardır:

  1. Blokaj ve Deadlock Riski: PipeWriter‘a yazma işlemi, ilgili PipeReader tarafından okunana kadar bloke olabilir. Eğer okuma ve yazma işlemleri aynı gorutinde gerçekleştirilirse, deadlock’a yol açabilir. Bu yüzden, okuma ve yazma işlemlerinin farklı goroutine’lerde yapılması önerilir.
  2. Hata Yönetimi: PipeWriter üzerine yazma işlemi sırasında veya PipeReader üzerinden okuma işlemi sırasında oluşan hatalar, dikkatli bir şekilde yönetilmelidir. Yazma veya okuma işlemi başarısız olursa, ilgili hata nesnesi ilgili yöntem tarafından döndürülür.
  3. Kaynakların Yönetimi: PipeReader ve PipeWriter kullanımdan kaldırıldığında, Close metodları çağrılarak kaynakların serbest bırakılması gerekir. Bu, kaynak sızıntılarını önlemek için önemlidir.

io.Pipe kullanımına dair basit bir örnek:

os.Pipe nedir?

os.Pipe, Go’nun os paketi içinde yer alır ve işletim sistemi seviyesinde bir pipe mekanizması sağlar. Bu yapı, işletim sisteminin sunduğu pipe mekanizmasını kullanarak iki dosya tanıtıcısı (file descriptor) arasında bir veri akışı kanalı oluşturur. os.Pipe genellikle, harici komutları çalıştırırken ve işletim sistemi seviyesindeki işlemler sırasında kullanılır. Örneğin, bir çocuk süreç (child process) ile ebeveyn süreç (parent process) arasında veri iletişimi kurmak için kullanılabilir. os.Pipe doğrudan işletim sistemi kaynaklarını kullanır ve bu nedenle işletim sistemi ile daha yakın bir etkileşime sahiptir.

io.Copy Kullanımı

io.Copy fonksiyonu, Go’nun io paketinde bulunan ve bir veri akışını bir io.Reader arayüzünü uygulayan bir kaynaktan, io.Writer arayüzünü uygulayan bir hedefe etkin bir şekilde aktarmak için tasarlanmış bir araçtır. Bu fonksiyon, genellikle dosya okuma/yazma işlemleri, ağ üzerinden veri alışverişi ve benzeri I/O işlemlerinde kullanılır. io.Copy‘nin temel avantajı, veri aktarım işleminin “buffering” mekanizması üzerinden otomatik olarak yönetilmesi ve büyük veri kümelerinin belleğe yüklenmesine gerek kalmadan akışkan bir şekilde transfer edilmesidir. Bu, özellikle kaynak ve hedef arasında büyük boyutlu verilerin aktarılması gerektiğinde önemli bir performans ve verimlilik kazancı sağlar.

Örnekler

HTTP Request ile Dosya Gönderme

io.Pipe kullanarak bir veriyi stream edip bir HTTP request’inde nasıl kullanabileceğinize dair bir örnek yapalım. Bu örnekte, yerel bir dosyayı okuyup bu veriyi http.Post fonksiyonu ile bir HTTP sunucusuna gönderen basit bir Go programı yazacağız. Bu işlem sırasında, io.Pipe kullanarak dosya içeriğini doğrudan HTTP request body’sine stream edeceğiz, böylece büyük dosyalarla çalışırken bellek kullanımını optimize edebiliriz.

HTTP Server ile Dosya Serve Etme

HTTP üzerinden büyük dosyaları stream etmek, sunucu kaynaklarını etkili bir şekilde yönetmek ve kullanıcılara hızlı bir deneyim sunmak için etkili bir yöntemdir. Geleneksel dosya sunumundan farklı olarak, streaming ile dosya, diskten okunduğu gibi parça parça gönderilir. Bu yöntem, özellikle büyük dosyalarla çalışırken veya hafızada üretilen üretilen içeriği sunarken server belleğinin daha az kullanılmasını sağlar.

Streaming, özellikle aşağıdaki senaryolarda faydalıdır:

  • Büyük dosyaların işlenmesi: Sunucu üzerindeki bellek kullanımını azaltır çünkü tüm dosyanın belleğe yüklenmesine gerek yoktur.
  • Gerçek zamanlı veri sunumu: İçeriğin sürekli güncellendiği canlı veri akışları veya gerçek zamanlı loglar için idealdir. Finansal piyasalar, sosyal medya akışları, video ve ses yayınları bunlara örnek olarak verilebilir.
  • Ağ verimliliği önemli olduğunda: İstemciler, tüm dosya indirilmeden içeriği işlemeye veya görüntülemeye başlayabilir.
  • Medya için adaptif bitrate streaming: İstemcinin bant genişliğine göre video veya ses içeriğinin kalitesinin gerçek zamanlı olarak ayarlanması.



Erhan Yakut Software Developer @Binalyze | Founder @Passwall | Golang Enthusiast | Open Sorcerer

Go Programlama Dili Kaynakları

Her gün çevremde Go Programlama Dili‘ni merak eden ama nasıl ve nereden başlayacağını bilemeyen birçok kişiyle karşılaşıyorum. Twitter mesaj kutum da bu konuların sorulduğu...
Erhan Yakut
4 min read